From 85a76912d3484b727af2e0c310b7508a10541ce3 Mon Sep 17 00:00:00 2001
From: bracesproul
Date: Tue, 15 Oct 2024 11:33:40 -0700
Subject: [PATCH 001/149] fix(sdk-js): Pass api key in headers by default if in
env
---
libs/sdk-js/src/client.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/libs/sdk-js/src/client.ts b/libs/sdk-js/src/client.ts
index c17e15ed5..133da19b3 100644
--- a/libs/sdk-js/src/client.ts
+++ b/libs/sdk-js/src/client.ts
@@ -60,6 +60,9 @@ class BaseClient {
this.defaultHeaders = config?.defaultHeaders || {};
if (config?.apiKey != null) {
this.defaultHeaders["X-Api-Key"] = config.apiKey;
+ } else if (process.env.LANGCHAIN_API_KEY) {
+ // Attempt to read the API key from the environment
+ this.defaultHeaders["X-Api-Key"] = process.env.LANGCHAIN_API_KEY;
}
}
From 7352ab14a24a3e14b2ac5b2f4795f6a555e5c161 Mon Sep 17 00:00:00 2001
From: bracesproul
Date: Tue, 15 Oct 2024 11:36:37 -0700
Subject: [PATCH 002/149] cr
---
libs/sdk-js/src/client.ts | 36 ++++++++++++++++++++++++++++++------
1 file changed, 30 insertions(+), 6 deletions(-)
diff --git a/libs/sdk-js/src/client.ts b/libs/sdk-js/src/client.ts
index 133da19b3..48cdc3368 100644
--- a/libs/sdk-js/src/client.ts
+++ b/libs/sdk-js/src/client.ts
@@ -31,6 +31,35 @@ import {
OnConflictBehavior,
} from "./types.js";
+/**
+ * Get the API key from the environment.
+ * Precedence:
+ * 1. explicit argument
+ * 2. LANGGRAPH_API_KEY
+ * 3. LANGSMITH_API_KEY
+ * 4. LANGCHAIN_API_KEY
+ *
+ * @param apiKey - Optional API key provided as an argument
+ * @returns The API key if found, otherwise undefined
+ */
+export function getApiKey(apiKey?: string): string | undefined {
+ if (apiKey) {
+ return apiKey;
+ }
+
+ const prefixes = ["LANGGRAPH", "LANGSMITH", "LANGCHAIN"];
+
+ for (const prefix of prefixes) {
+ const envKey = process.env[`${prefix}_API_KEY`];
+ if (envKey) {
+ // Remove surrounding quotes
+ return envKey.trim().replace(/^["']|["']$/g, "");
+ }
+ }
+
+ return undefined;
+}
+
interface ClientConfig {
apiUrl?: string;
apiKey?: string;
@@ -58,12 +87,7 @@ class BaseClient {
this.timeoutMs = config?.timeoutMs || 12_000;
this.apiUrl = config?.apiUrl || "http://localhost:8123";
this.defaultHeaders = config?.defaultHeaders || {};
- if (config?.apiKey != null) {
- this.defaultHeaders["X-Api-Key"] = config.apiKey;
- } else if (process.env.LANGCHAIN_API_KEY) {
- // Attempt to read the API key from the environment
- this.defaultHeaders["X-Api-Key"] = process.env.LANGCHAIN_API_KEY;
- }
+ this.defaultHeaders["X-Api-Key"] = getApiKey(config?.apiKey);
}
protected prepareFetchOptions(
From 433c38228009a12993c8b905bbfc0b46feecfeba Mon Sep 17 00:00:00 2001
From: bracesproul
Date: Tue, 15 Oct 2024 11:37:20 -0700
Subject: [PATCH 003/149] cr
---
libs/sdk-js/src/client.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/libs/sdk-js/src/client.ts b/libs/sdk-js/src/client.ts
index 48cdc3368..36f1176eb 100644
--- a/libs/sdk-js/src/client.ts
+++ b/libs/sdk-js/src/client.ts
@@ -87,7 +87,10 @@ class BaseClient {
this.timeoutMs = config?.timeoutMs || 12_000;
this.apiUrl = config?.apiUrl || "http://localhost:8123";
this.defaultHeaders = config?.defaultHeaders || {};
- this.defaultHeaders["X-Api-Key"] = getApiKey(config?.apiKey);
+ const apiKey = getApiKey(config?.apiKey);
+ if (apiKey) {
+ this.defaultHeaders["X-Api-Key"] = apiKey;
+ }
}
protected prepareFetchOptions(
From 26d18d3ca59f20f9cde3050d885fb83a638485a7 Mon Sep 17 00:00:00 2001
From: Harrison Chase
Date: Tue, 19 Nov 2024 11:44:17 -0500
Subject: [PATCH 004/149] add how to guides for autogen integration (#2466)
---
docs/docs/how-tos/autogen-integration.ipynb | 334 ++++++++++++++++++
.../how-tos/autogen-langgraph-platform.ipynb | 173 +++++++++
docs/docs/how-tos/configuration.ipynb | 2 +-
docs/docs/how-tos/index.md | 2 +
poetry.lock | 167 ++++++++-
pyproject.toml | 1 +
6 files changed, 667 insertions(+), 12 deletions(-)
create mode 100644 docs/docs/how-tos/autogen-integration.ipynb
create mode 100644 docs/docs/how-tos/autogen-langgraph-platform.ipynb
diff --git a/docs/docs/how-tos/autogen-integration.ipynb b/docs/docs/how-tos/autogen-integration.ipynb
new file mode 100644
index 000000000..1673014d4
--- /dev/null
+++ b/docs/docs/how-tos/autogen-integration.ipynb
@@ -0,0 +1,334 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "100c0c81-6a9f-4ba1-b1a8-42aae82b7172",
+ "metadata": {},
+ "source": [
+ "# How to integrate LangGraph with AutoGen, CrewAI, and other frameworks\n",
+ "\n",
+ "LangGraph is a framework for building agentic and multi-agent applications. This includes integrating with other agent frameworks.\n",
+ "\n",
+ "This guides shows how to integrate LangGraph with other frameworks. The framework we show off integrating with is AutoGen, but this can easily be done with other frameworks.\n",
+ "\n",
+ "At a high level, the way this works is by wrapping the other agent inside a LangGraph node. LangGraph nodes can be anything - arbitrary code. This makes it easy to define an AutoGen (or CrewAI, or LlamaIndex, or other framework) agent and then reference it inside your graph. This allows you to create multi-agent systems where some of the sub-agents are actually defined in other frameworks."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b189ceb2-132b-4c7b-81b4-c7b8b062f833",
+ "metadata": {},
+ "source": [
+ "## Setup"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "62417d3a-94f9-4a52-9962-12639d714966",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# %pip install autogen bs4 langgraph langchain-openai langchain-community"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d46da41d-0a71-4654-aec8-9e6ad8765236",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# import os\n",
+ "# import getpass\n",
+ "\n",
+ "# os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()\n",
+ "# os.environ[\"TAVILY_API_KEY\"] = getpass.getpass()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1926bbc3-6b06-41e0-9604-860a2bbf8fa3",
+ "metadata": {},
+ "source": [
+ "## Define AutoGen agent\n",
+ "\n",
+ "Here we define our AutoGen agent. From https://github.com/microsoft/autogen/blob/0.2/notebook/agentchat_web_info.ipynb"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "524de117-ff09-4b26-bfe8-a9f85a46ffd5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import autogen\n",
+ "import os\n",
+ "\n",
+ "config_list = [{\"model\": \"gpt-4o\", \"api_key\": os.environ[\"OPENAI_API_KEY\"]}]\n",
+ "\n",
+ "llm_config = {\n",
+ " \"timeout\": 600,\n",
+ " \"cache_seed\": 42,\n",
+ " \"config_list\": config_list,\n",
+ " \"temperature\": 0,\n",
+ "}\n",
+ "\n",
+ "autogen_agent = autogen.AssistantAgent(\n",
+ " name=\"assistant\",\n",
+ " llm_config=llm_config,\n",
+ ")\n",
+ "\n",
+ "user_proxy = autogen.UserProxyAgent(\n",
+ " name=\"user_proxy\",\n",
+ " human_input_mode=\"NEVER\",\n",
+ " max_consecutive_auto_reply=10,\n",
+ " is_termination_msg=lambda x: x.get(\"content\", \"\").rstrip().endswith(\"TERMINATE\"),\n",
+ " code_execution_config={\n",
+ " \"work_dir\": \"web\",\n",
+ " \"use_docker\": False,\n",
+ " }, # Please set use_docker=True if docker is available to run the generated code. Using docker is safer than running the generated code directly.\n",
+ " llm_config=llm_config,\n",
+ " system_message=\"Reply TERMINATE if the task has been solved at full satisfaction. Otherwise, reply CONTINUE, or the reason why the task is not solved yet.\",\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8aa858e2-4acb-4f75-be20-b9ccbbcb5073",
+ "metadata": {},
+ "source": [
+ "---"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d6bc7b69-4a36-44dc-a501-7e17122cc385",
+ "metadata": {},
+ "source": [
+ "## Define LangGraph agent\n",
+ "\n",
+ "We now define our LangGraph agent. We will create a simple ReAct-style agent with a web search tool"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a0fcdaac-8fbe-4589-8e61-8092165356cd",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langgraph.graph import StateGraph, START, MessagesState\n",
+ "from langgraph.prebuilt import ToolNode, create_react_agent\n",
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "from langchain_openai import ChatOpenAI\n",
+ "from langchain_core.messages import HumanMessage, AIMessage\n",
+ "\n",
+ "model = ChatOpenAI(model=\"gpt-4o\")\n",
+ "tools = [TavilySearchResults(max_results=1)]\n",
+ "web_search_agent = create_react_agent(\n",
+ " model, tools, state_modifier=\"You are an agent specializing in web search\"\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "dcc478f5-4a35-43f8-bf59-9cb71289cd00",
+ "metadata": {},
+ "source": [
+ "## Create the multi-agent graph\n",
+ "\n",
+ "We will now create our multi-agent system combining the AutoGen agent with the LangGraph agent. We can do this by creating a graph that routes user query to the appropriate agent and executes the agent"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d129e4e1-3766-429a-b806-cde3d8bc0469",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from typing import Literal, TypedDict\n",
+ "\n",
+ "\n",
+ "class Route(TypedDict):\n",
+ " \"\"\"Decide where to go next\"\"\"\n",
+ "\n",
+ " goto: Literal[\"web_search_assistant\", \"coding_assistant\"]\n",
+ "\n",
+ "\n",
+ "def route(state: MessagesState) -> Literal[\"web_search_assistant\", \"coding_assistant\"]:\n",
+ " messages = [\n",
+ " {\n",
+ " \"role\": \"system\",\n",
+ " \"content\": \"Based on the conversation so far, decide who to call next: web search assistant or coding assistant.\",\n",
+ " }\n",
+ " ] + state[\"messages\"]\n",
+ " response = model.with_structured_output(Route).invoke(messages)\n",
+ " return response[\"goto\"]\n",
+ "\n",
+ "\n",
+ "def call_autogen_agent(state: MessagesState):\n",
+ " last_message = state[\"messages\"][-1]\n",
+ " response = user_proxy.initiate_chat(autogen_agent, message=last_message.content)\n",
+ " # get the final response from the agent\n",
+ " content = response.chat_history[-1][\"content\"]\n",
+ " return {\"messages\": AIMessage(content=content)}\n",
+ "\n",
+ "\n",
+ "builder = StateGraph(MessagesState)\n",
+ "builder.add_conditional_edges(START, route)\n",
+ "builder.add_node(\"coding_assistant\", call_autogen_agent)\n",
+ "builder.add_node(\"web_search_assistant\", web_search_agent)\n",
+ "graph = builder.compile()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "c761fc05-e8b6-4905-a793-eb7522d20060",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCACVAY4DASIAAhEBAxEB/8QAHQABAAIDAQEBAQAAAAAAAAAAAAUGAwQHCAIBCf/EAEkQAAEEAQIDBQQFBwoEBwEAAAEAAgMEBQYRBxIhExUxlNMUIlRWCBY2QVEjMlVhdLLRNVJxcnN1gZOhswlCYrQzU4OFkaKx0v/EABkBAQEBAQEBAAAAAAAAAAAAAAABAgQDBf/EAC8RAQABAgIHBwUAAwAAAAAAAAABAhESUQMUMWKRktEEITNBcaGxEyNSYcEy4fD/2gAMAwEAAhEDEQA/AP6poiICIiAiIgIiICIiAiIgIiICIiAiIgIiICLSzGXgwlB9qcPkAIayKJvNJK8nZrGD73E9B/rsOqhBpabUQ7fUU0ksbx7uIhlLa0Q/B3LsZXfjzEt/Bo8T600RMYqptH/bFsmbGoMXUkLJ8lUheOhbJO1pH+BKxfWrC/pih5ln8Vir6M0/Uj7ODBY2Fn82OnG0f/ACy/VXC/oeh5Zn8Fv7P79l7j61YX9MUPMs/in1qwv6YoeZZ/FPqrhf0PQ8sz+CfVXC/oeh5Zn8E+z+/Y7j61YX9MUPMs/in1qwv6YoeZZ/FPqrhf0PQ8sz+CfVXC/oeh5Zn8E+z+/Y7j61YX9MUPMs/ivqPUuIleGMytJ7j4NbYYSf9V8/VXC/oeh5Zn8F8SaRwUzCyTC457T4tdUjI/8AxPs/v2TuSwcHAEEEHqCPvX6qy7RMGNcZ9PSnB2Ny7sYRvVlJ+58Phtv97OV36/FSWCzRy0U0c8BpZGs7s7VQu5uR33Oa7Yc7HDq12w3HQhrg5rc1URbFRN49y2SUREXigiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiCsTEZfiBFXeA6HD022w07/8Ajzukja78N2sjlH/qlWdVmo32PiNkg7fa/ja8kR26EwyStk6/qEsXT9asy6NNtpiNlo/373WRERc6Ob0PpC6EzNnOVsVl5spZw9ezZsNqY+1Ix7YDyy9k8RFsxa4hpERcdyBsoThv9JzTOs+EFfXmVbbwNZsMDrsMuPtubDJKdmRxOMINjckAOiDgSR+KpHCuDNYfinPp/R+E1ZiOH1qHIzZPG6oxxgqY206QOjdQmPV7JXvkcY2ue0A8w5SdhXtMZnW+E+jRpfSGP07rDT+X07PQxmo5amKf7V7EHvZYfj3EETu2Y080fMQ1+46+Ad4q/SB0Bc0Hk9ZR6hYNO4udta/ZkrTMkqSucxoZLC5gkYd5GfnNHRwPh1VT1p9LDS2mpNIyUq+TylDOZd+NfaZh747JjIDK6WJorkzg7xhvJuHBznNLgx23DMroXMZHRHHWnQ0rrKalnbWAt4xmoILFq5eiZLDHM4l5e8kdk4ljyHtZyktaPDv30i6WRrT8N9R0MNkM5U07qaO9fq4mu6xZbXdVsQmRkTfefyulZuGgnbc7dEHXaNyLI0q9qHn7GeNsrO0jdG7lcNxu1wDmnY+BAI+8LOtLC5RubxFLIMr2ajLULJhXuwuhnjDgDyvY7q1w32IPgVuoCrGoNsTqrAZKPZvtcjsZZ8ffY5j3xn9fK9uw/ASO/HrZ1WNYD2vKaYot3MkmRFh2w35WRRveXH8Bzcg/pcF0aD/O3lafhY2rOiIudBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEWKezDWDTNKyIOJDedwG5ALjt/gCf6AVX4NWWM7BDJgcZParWqUlmvkbg9mrB4O0cb2u/Le8eu4jI5Rvv1aCG7qPDS5FlW3SdHHlaEhmqvlJDHEtLXRvI68jmkg9DseV2xLQFhgv4rWeOv4m9WjkMkLq+Qw95rXODHgtcyRnUOY4EjcbtcPAkLLi8ZlGXYL2Ryz5ZPYo4JcfWiYyoJ995Jmbgybk9AHPIDR4bkk5s1prG6gEft1YSSR79nPG90c0f48kjCHN/wIXtTVTMYa+OS+qlD6NnCdpBHDfSwI8CMTACP/qvqH6OPCqtNHLFw50vHLG4OY9uJgBaR1BB5VPfUZ7ARDqTPQs+5vtbZNv8AF7HH/VPqTY+as9/nQ+ktfT0f5+0lozWhFV/qTY+as9/nQ+kn1JsfNWe/zofST6ej/P2ktGa0Iqv9SbHzVnv86H0k+pNj5qz3+dD6SfT0f5+0lozRea4C8N9R5W1k8roPTuRyNp5knt2sZDJLK4+LnOLdyf1laZ+jZwnPjw30sf8A2iD/APlWD6k2PmrPf50PpL8+o8zuj9T557fvb7RG3f8AxbGD/qn09H+ftJaM2zTqad4aadrY+hUqYPEQFza1CjAGN5nOLyyKJg3c5zi48rQSST0K+sHjrFnIzZzJRdhcmjENeqTuasG4dyuIJBkc7q/l6dGtBdyczsuJ0ji8PaNuKF894gg3Lkz7E+x8QHvJLQenut2HQdOgWzksbZuXMfPXydmg2tKXywQtjdHaYQQWSczSQPAgsLSCB1IJBk1U0xMUefmeiQRVypl85jhjq+YxYuyze0GzkMRsa8AZ1jLo3u7XeRvTZgk2cNidiCpPDZ+hqCjXt0LLZ4Z4hMwEFj+UkjcscA5vUEbEAggjxC8ESCIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICKByuqDDNbo4qjLlsvFUFqOAB0UDw53I0OsFpY0khxIG7g1pPKem+O1pq5nHXY8xkpHY+Z9d8NLHukqmHkG72umY8PkD3+I91paA0tILuYNm/q3HUpmwMfLkLPtkVGSDHxOsvgkeOYdqGA9k0N94ufygDbr1AOCH6x5OeN8oqYOvBfk5omk25LdVo2YebZghc53UgCTZoA3BceWZqUK1ASitXiriWR00giYG873fnOO3i4/eT1KzoILFaLxeMdj5pInZTI0O29nyeTd7Tbj7Ygyhsrt3MDtmgtbs3ZrQAA0ATqIgIiICIiAiIgIiICIiAiIgKLyumMZmZnWLFRovGrLTZfgJitRRSbc7Y5mbPZuQ0+64dWtPiARKIgrMlDUGBrSHG2os5DXoRw16GScY5pZ2HYvfaHN+c3xBjPvAHcAkLa+uFCvYuw5HtsQaYgMk9+Mw1nGYhrGxzn8m93OeQta4kOLdwOZvNOLBeo1snUlq3K8VurM0skgnYHse38C09CP6UGdFAWNNWK1mzaxGTno2LVuKxYZac+1A5rRyvYyNz9oudv/l8o5gHEEl3Mg1NPUtR1szjpMdLZvSVKclcuswzMA5o5HvawdlzN3Gz9gHAtDnbtLgn0XzHI2WNr2OD2OAc1zTuCD4EFfSAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiINe9frYyrJZuWYqlaPbnmneGMbudhuT0HUgKHGPvagkZLkDLjqTHWYX4xj2Pbbjd7kb5HAbt90OcGtI27Qc25b0x5uzFZ1jgMTJerM54bOQOOmrdo+yIXQtEjXnozs3zxn+cS5u3QFWRBr4/H1cTQrUaNaGlSrRthgrV4xHHFG0bNY1o6NaAAAB0AC2ERAREQEREBERAREQEREBERAREQEREBERAREQEREFbOnX6arNfpuNkFarWnEWAjDIas8rndo083ITEebmG7fd2kO7Ts3lmcdlK+TjkMEjHSQuEc8Ika58EnK1xjfykgOAc07b/eD4ELbVbs2Ysdr/HwC9WruyVGw51D2b8tZfE6HaUSj7mNeWlrvHtGkbcp3CyIiICIiAiIgIiICIiAi/HODGlziGtA3JJ6AKlHWGbywFjC4yica/rDYyFl8ckzfueI2xnlafEbncjxAXto9FVpb4ei2uuyKkd+6w+Awfm5vTTv3WHwGD83N6a9tVrzjjBZd0VI791h8Bg/NzemnfusPgMH5ub001WvOOMFl3RUjv3WHwGD83N6ad+6w+Awfm5vTTVa844wWXdFSO/dYfAYPzc3pp37rD4DB+bm9NNVrzjjBZd0VI791h8Bg/NzemnfusPgMH5ub001WvOOMFnlDiR/xDMrojj1c0e3htPblxdm3iG1vb2tmvyPmiFedjuxJY1zGOPIN+btmnf3Bv7dxstmfHVZbtdlS4+Jjp68cvatikIHM0P2HMAdxvsN9t9gvOOpfo/S6o4/YHivax2GGYxVfszVFiTsrErRtDM89lvzRg9P6rP5vXsHfusPgMH5ub001WvOOMFl3RUjv3WHwGD83N6ad+6w+Awfm5vTTVa844wWXdFSO/dYfAYPzc3pp37rD4DB+bm9NNVrzjjBZd0VI791h8Bg/NzemnfusPgMH5ub001WvOOMFl3RUjv3WHwGD83N6ad+6w+Awfm5vTTVa844wWXdFSO/dYfAYPzc3pr9bqbVFP8AK28PjrVdvWRlG4/tuX7+Rr4wHH9Rc3f8U1XSZxxgsuyLXx+Qr5WjXuVJRNWnYJI5ACOZpG46HqP6CthckxMTaUERFAREQEVe1BqeejdGOxdOO/kuzE0gnlMUMLCSGl7w1x3JB2aAd9jvsOqie/dYfAYPzc3prpp7PXVGLuj1mFsu6Kkd+6w+Awfm5vTTv3WHwGD83N6a3qteccYLLuipHfusPgMH5ub00791h8Bg/Nzemmq15xxgsu6Kkd+6w+Awfm5vTTv3WHwGD83N6aarXnHGCy7rw3xc/wCIVleGvHK3pGXhvLO/EWZ6HY94NEl8SOj9nmYTCTGHNHNyjm37QDf3evq3v3WHwGD83N6a4/rX6P02ueOeleKF7H4ZuWwcRaarbEvZ2nt3MEjz2e+8ZJI8d9m/c3q1WvOOMFnovDWbdzEUbGQptx9+WCOSxUbL2ogkLQXMD9hzBp3HNsN9t9gtxUjv3WHwGD83N6ad+6w+Awfm5vTTVa844wWXdFSO/dYfAYPzc3pp37rD4DB+bm9NNVrzjjBZd0VI791h8Bg/NzemnfusPgMH5ub001WvOOMFl3RUjv3WHwGD83N6ad+6w+Awfm5vTTVa844wWXdFVMbqy/BerVM5Rr1Raf2UFqnO6WMybbhjw5jSwnY7HqCRtuCWg2tc+k0dWjm1RZF6oJbpnLkHYinMQR/UKr2mQBpvFAAACpFsB/UCsOqvsxmP2Ob9wqvaa+zmK/ZIv3Au3Q+DPr/DySSIi0giKHzmrsTpu/haWRt+zWczbNGizs3u7abs3ycu7QQ33I3nd2w6bb7kKCYREVBERAREQEUPb1diaOqcdpye3yZnIVprdat2bz2kURYJHcwHKNjIzoSCd+m+xUwoCIioIiiW6qxb9VSabFknNR0m5B1bsn7CBz3Rh/Pty/nNcNt9+m+2yglkWjmc5j9PUfbMpdgx9XtGRdtYkDG873BjG7n73Oc1oH3kgLeVBEWjlc5j8Gys7I3YKTbViOpAZ5AztZnnZkbd/Fzj4AdUG8iIg1+Fx30Ljf1GUD9QEr9la1VOFv2Fx39ab/eerWuXtPj1+s/KztkREXMgiIgokJ319qX9UVQf4cj/AOJUwoeD7fam/s6n7j1ML61fl6U/ENTtERFhkRFUYOK+lbFCC7HlC+tPmXafjeK0vW8JXRGLbk3HvtcOY+703326qC3IiKgiIgIiICKJ0tqrF61wVfMYaybmOsOkbHMYnx8xY9zHe68Bw2c1w6j7vwWwM5jzmzhxdgOVFcWzSEg7UQl3IJC3xDS4EA+BIP4FQbyIioItGDOY+1l7eKhuwS5KpFHNYqMkBkhZIXCNzm+IDuR+2/jylbyCA1o4tx+OI2374xo32/G7CD/oSuhLnmtv5Nx398Yz/voF0NefaPDo9Z/i+SL1V9mMx+xzfuFV7TX2cxX7JF+4FYdVfZjMfsc37hVe019nMV+yRfuBa0Pgz6/w8mzk6IyeNt0zNNWFiF8JmrSGOWPmaRzMcOrXDfcEdQV4/wBOcetX4cads5WzYlwvDkHD68nlD3vszSTvqRzbncvMYhjsOI392fxXslV2zw905bxmpMfLiYHU9Rue/LRdQLbnxNhcXbHoSxjRuNvDfx6qTEzsR5dZn+Imo2cOdOm5eFzV9PJartwv1BNi5XB0rHwU4rDYpXxshhlbvEwN35d9wAQZDUeitZgcMsDrbL2Inza3l7vtY7Lvs3YKZx1giN9swxOc8OEg5+Xm5SOu43XorW/C3S3EXG0qOfxMdyCjIJaj4pH15azgNt4pYnNezp091w3CirnAXQl/S+L09PgufE4yy+7UiFucPineHh0vaB/OXntHnmLidzv4gFZwyKxwRuZXCcRuJGhrOcyGo8RgHY+xQu5af2i1D7TE9768kp6v5Sxrml27gJACT0Vp444TVWoeHN+no27JSzRlhf8AkbPsss0LZWmWGOfY9k97A5oft0JHh4jHS4ZTcP8ABMxvDXuXTjJbD7Fx+Wp2Mg6y8gDnc/2iN7n9B7z3O6ADpssc/D7Pa2o2sRxCvYHO4GVrXtrYfH28dM2Zj2uY/tfa3kAbE+6Ad9jv02OrTaw4RleIeWzlLQmiNH29RRS5LL5Sll4dQ519TJQWKkTJDSN5sczgD2geHM3c5rAA8blWDIQ610Noe3gtWXMzYlzucrUdM08HqV82RD3RufJDNkJIInNi/JPfzlpeGkjckBdbk4BaAm0azSz9OQvwzLZvtYZ5e3Fknczifn7Xtf8Ar5+bbpvssh4E6Hdo8aYdhS7EC4MgA65YM7bI8JhY7TtRJsNuYP326b7LOGR5wm1LrqhoLVel7eocpi8pitdYbGVrzMs69arQWXVXmI2XMYZ2jtHfnt6g8ruYBdKz+n7c3F3TvDCHVmpcXp52Gt56xaZmJjfyE4njibALTnGRrGB5eWsI8R9wV/x/0ftA4qGxFUwAgjs26l+cNtz/AJWzWfzwzO9/3nh3Vzj1fsOfm2Cmde8LdL8TYaLNR4sXn0ZDLUsRzyV567iNnGOWJzXt3G24DgDsN/BXDI41rXhw21xw4ZaaOptRsghwGZL8jHknNvzN7aqQx1gDn23I6tIds0Ak9d759HLN5PK6GydLLZGxmLOEz2TwrL9x3NPPFXtPjjdI7/mfyhoJ+/bc9VZsBwo0rpi3hbWMxXs0+GrWKlF/tErzFHO9skwPM485c9jXFztzvv16neOm0LndMGeHQNzBYKlct2Mjdiy2Os33y2ppDJJI1wtR8oJJPLsQPu2HRW1puIX6QOWJo6b05TdnpM7ncg6KjVwGU7sfN2cL5JO2s7ExxNYC48o5iQ0AHqFyTTw4nax4S5bE1sjk7eR0xrWajeq1M3yZG5jo42vNaO+WsLpA6VvvkMLhHsSN1261wwt69oey8R5cRnm1p2WcdLg6trFzVZAHNc4Si09+5DgPdc3puDvv0xn6OfDwYafFRafNShNdZknRU71mAi02LshM1zJAWvLNw5wILtyXbnqpMTM3HGNT5e/mtGaW1nic1rm9w0xmOuR5aOjlDVzlKxHMQ6xY3I9oEIZIx0ZJ/N5tnjfeb1L9ZdScT9b43QerMhBLm9BU8pin3L0r6sNiSxLGJImO3EJfHGwbtaCCS7bfddJyX0beHGVxeLxs+m2toY2B9WvXguWIWdk95e9kgZIO1DnEucJObmJJO+6ns1wl0lqCzenvYdksl3Fx4WcMlkjaabJHSMiDWuAaA57iC0B3XbfYBMMjzHr32DUHBHK4O3b1jj87g9V4ZuUx2ezclixUfLYrtAZYY/8AKwua8yMO52ds4Bpa0Do/FXFWJ8/p3h9pi5rC7mqmMmyD3Qarlx7GVzIGNms2nNllmfzgtYzZw25uYbALo1DgToXHaRzGmY8BHLh8w4PyEVqxNPLZcA3lc+aR7pCW8reU827dhtstW39HnQN+ni61jDTzMxscsNeR+TtmUxSP55IpJO155Y3O6lkhc39SmGRxXR2p9RcVKnACvltS5ekM5hsw7LuxF11V150Hs4Y5zmbEHcE8zOVw5nAEBxBhtS1LWrNF6fxOazmauDBcWG4Cve7ymisvrCxtGZJWOBdI0ODWyH3htuCCV6YwHB/SGlp8FLisO2kcF7WMayOeXkqi04Onaxhdy8ri0bN22bt7oavnIcG9HZXT2awdvCsnxmYyD8rchdPLu+25weZmv5uaN3M0Echbtt02TDPmLTiMbHhsXUoRS2J4q0TYWy253zzPDRsC+R5LnuO3VziST1K21HaewFLS2Gq4rHMljpVm8kTZp5J3gbk9XyOc5x3J6kkqRXoNfhb9hcd/Wm/3nq1qqcLfsLjv603+89Wtc3afHr9Z+VnbIiIuZBERBRIPt9qb+zqfuPUwoeD7fam/s6n7j1ML61fl6U/ENVbXDdRw5Didx8zGj7epMzp3B4PB1b8NTB3nUpr008srXSulZs8sjETW8oO3M7rv4LSs4LI634xZHQ1zWGpMXhdNadoz1nY7Juq28hNM+Vr7M0zADJy9i0cv5vM4kg77Lpuu+D2keJV2ld1Bifab9Nro4Lle1NVnYx3Us7SF7HFh/mkkfqWhqHgFoLVFHEVL+BBixVT2Cm6rbnrSR1tgOxL4ntc+PoPccSP1LxtLLhPDDWmpOO1/Q2mM3qnKYulHgLmVtXcJZNKzmZYcg+nG4ys2c1nJGJXBhHMZR92yjtIV7unNF6LfSzuZilg4r3MZM9l+SMXoZL8wkFlrCGyl3Zg+8PvdsBuV6O1HwO0PqrG4Khf0/Cyvgm8mM9hmlpyU2coaWRvhcxzWkAAt32Ow3BX3i+Cmi8JgsZhqWFFfG43K991IBZmIiuc7n9puX7n3nOPKSW9fDZZwyPPOWyOoKOgOIOvo9X6iOW09rm1Vo1HZKT2JtVuTZGa74PzZGFkjgOfctHKGloAC1uKua1DndU8QMQNRarp68hzFSrpvT2IsWIaU+NeIfyjhFs0hwNkvlc4FnJ0LdgD6Xs8JNJ29NZrT8uK58Rmb78per+0yjtrL5hM5/MH8zd5Gh2zSB02226LjvE/6POrdWa4zuT09Lh8AMnLHLHnauby1a7WeI2MMpqxSCvNIAzoTyggNDgdtzJpmw9IjoF5R+kXqTPWsrr7JaMu6lr3dE45k922zUHsWOqzCH2hrGVBG8WnFhaXiTZuzgA4FdtmxHFNsrxX1TpL2cOIj9o07ZfJy/dzObeaC7bxIaBv9w8Fgy3ATSOsrbsrqzC1MtnLdaODJS1nz16l1zG8oc+v2pa7l/wCUv53NAGzugW6omYtA5pqbJ53BcTsTrHVmT1FFojJd1Mxs2ByRip46w/lD4btbp2kc0jgO02dsHBvu9CsujamXwXFfI6c19mdVDLakkyQw96pl3nFXK3V4ZFE0g1Z4Yj02A6tc4Pd0XSD9HrQDs1jcrJgnT3Me2s2Az3rMke9djWQOfG6QskewNbs94c7pvvv1W7p7gjorSurZNTY3C9lmnumeLEtqeYROmO8piY97mRl535iwDfc7qYZHnjEfXjPcCuHGou9tUZrA45uTfn4cNmXwZew0TvbDM2Zzg6YRBjt4y8c24/O22Vm0jhMLrX6TWL1Bj87nrdKfQuNy1SfvWxF7S0WXtb2sbXNDmOa1rnRlvKXOcS3dx36nkvo78P8ALafxGEsYOTu3EtnZThiyNqIxtnfzzNL2Shzmvd1LXEjoBtsFI5fgtovNXMBbnwjIbGBhbWx0lKxLVMELS0ti/JPbzRgtb7jt29PBTDI886l1nqEa1oa20vc1G3TbtaV8HPYymoC6paY62K08UOOEZaIw4vDZC5rwW77FfmqcjqCtoLjBrqLV+oo8rpXVVqPF1WZKQU44Y5IHGF0P5sjHCRzdn82w25eXZdzyP0cOHWWyV2/a04H2Ldr25/LcsMYyzzh5niY2QNilLhuZIw1x3O5O53m7nCTSd/TmpMDPiu0xOorcl7KV/aZR7RNJy87uYP5m78jejSB06DxTDI5dpHRdSx9LbiLkX5DMMnrY3D2o4I8rYZBIXi00tfEH8r2DlGzHAtaSSACTv6AVSznCnS+o9Y47VV7GudqDHtYyC7BamgcWMfztZII3tbK0O3Ia8OHU9Oqtq3EWFf1t/JuO/vjGf99Auhrnmtv5Nx398Yz/AL6BdDWe0eHR6z/F8kXqr7MZj9jm/cKr2mvs5iv2SL9wK42II7UEkMreeKRpY5p+8EbEKhw1c/pmvDjm4SbOV67GxQ3KdiFrnsA2b2jZXs2fsOuxIPj035RezzE0TRe03v3zb5WO+LJ1FCd7Z75MyvmqXrp3tnvkzK+apeuvfBvRzR1LJtFCd7Z75MyvmqXrp3tnvkzK+apeumDejmjqWTaKE72z3yZlfNUvXTvbPfJmV81S9dMG9HNHUsm0UJ3tnvkzK+apeune2e+TMr5ql66YN6OaOpZNooTvbPfJmV81S9dO9s98mZXzVL10wb0c0dSybRQne2e+TMr5ql66d7Z75MyvmqXrpg3o5o6lk2ihO9s98mZXzVL1072z3yZlfNUvXTBvRzR1LJtFCd7Z75MyvmqXrp3tnvkzK+apeumDejmjqWTaKE72z3yZlfNUvXTvbPfJmV81S9dMG9HNHUsm0UJ3tnvkzK+apeune2e+TMr5ql66YN6OaOpZNooTvbPfJmV81S9dfTLepLzhDBpmbHvd09oyFqAxR/8AURFI9ztv5o238Nx4hg3o5o6lkjwt+wuO/rTf7z1a1H6fw0ensLTx0T3SsrxhnaP/ADnn73H9ZO5/xUgvnaaqK9LVXGyZlJ75ERF4oIiIKJB9vtTf2dT9x6mFrZ/DZClmJcxiqzcgbETIbNIyiN55C7lfGT7u/vEFrttxsdxy7Ojjls9v9jcp5ql66+vFtJETExsiO+YjZFvOWp700ihO9s98mZXzVL1072z3yZlfNUvXTBvRzR1LJtFCd7Z75MyvmqXrp3tnvkzK+apeumDejmjqWTaKE72z3yZlfNUvXTvbPfJmV81S9dMG9HNHUsm0UJ3tnvkzK+apeune2e+TMr5ql66YN6OaOpZNooTvbPfJmV81S9dO9s98mZXzVL10wb0c0dSybRQne2e+TMr5ql66d7Z75MyvmqXrpg3o5o6lk2ihO9s98mZXzVL1072z3yZlfNUvXTBvRzR1LJtFCd7Z75MyvmqXrp3tnvkzK+apeumDejmjqWYtbfybjv74xn/fQLoapFbE5bUd6mchj3YfHVZmWnRzTMfNNIwhzG7Ruc1rQ4AkkknlAA67i7rl7RVGGmiJvMX97dEnZYREXEgiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiD/2Q==",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from IPython.display import display, Image\n",
+ "\n",
+ "display(Image(graph.get_graph().draw_mermaid_png()))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "23d629c3-1d6b-40af-adf6-915e15657566",
+ "metadata": {},
+ "source": [
+ "## Run the graph\n",
+ "\n",
+ "We can now run the graph. We can see in the examples below how we first route to the appropriate agent, then respond with the subagent."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "b528ddb9-ec12-433c-a174-33d94dc49d80",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\u001b[33muser_proxy\u001b[0m (to assistant):\n",
+ "\n",
+ "Find numbers between 10 and 30 in fibonacci sequence\n",
+ "\n",
+ "--------------------------------------------------------------------------------\n",
+ "\u001b[33massistant\u001b[0m (to user_proxy):\n",
+ "\n",
+ "To find numbers between 10 and 30 in the Fibonacci sequence, we can generate the Fibonacci sequence and check which numbers fall within this range. Here's a plan:\n",
+ "\n",
+ "1. Generate Fibonacci numbers starting from 0.\n",
+ "2. Continue generating until the numbers exceed 30.\n",
+ "3. Collect and print the numbers that are between 10 and 30.\n",
+ "\n",
+ "Let's implement this in Python:\n",
+ "\n",
+ "```python\n",
+ "# filename: fibonacci_range.py\n",
+ "\n",
+ "def fibonacci_sequence():\n",
+ " a, b = 0, 1\n",
+ " while a <= 30:\n",
+ " if 10 <= a <= 30:\n",
+ " print(a)\n",
+ " a, b = b, a + b\n",
+ "\n",
+ "fibonacci_sequence()\n",
+ "```\n",
+ "\n",
+ "Save this code in a file named `fibonacci_range.py` and execute it. It will print the Fibonacci numbers between 10 and 30. TERMINATE\n",
+ "\n",
+ "--------------------------------------------------------------------------------\n",
+ "{'coding_assistant': {'messages': AIMessage(content=\"To find numbers between 10 and 30 in the Fibonacci sequence, we can generate the Fibonacci sequence and check which numbers fall within this range. Here's a plan:\\n\\n1. Generate Fibonacci numbers starting from 0.\\n2. Continue generating until the numbers exceed 30.\\n3. Collect and print the numbers that are between 10 and 30.\\n\\nLet's implement this in Python:\\n\\n```python\\n# filename: fibonacci_range.py\\n\\ndef fibonacci_sequence():\\n a, b = 0, 1\\n while a <= 30:\\n if 10 <= a <= 30:\\n print(a)\\n a, b = b, a + b\\n\\nfibonacci_sequence()\\n```\\n\\nSave this code in a file named `fibonacci_range.py` and execute it. It will print the Fibonacci numbers between 10 and 30. TERMINATE\", additional_kwargs={}, response_metadata={}, id='e95a8aa1-5aa8-4ff2-ba74-2b2993ea0a5a')}}\n"
+ ]
+ }
+ ],
+ "source": [
+ "for chunk in graph.stream(\n",
+ " {\n",
+ " \"messages\": [\n",
+ " {\n",
+ " \"role\": \"user\",\n",
+ " \"content\": \"Find numbers between 10 and 30 in fibonacci sequence\",\n",
+ " }\n",
+ " ]\n",
+ " }\n",
+ "):\n",
+ " print(chunk)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "b120f9ba-f640-482b-a457-1893d6db5543",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "(('web_search_assistant:d08ae326-b6b2-1749-e8ea-4d308f22d819',), {'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_wZ5w5uO733Cc4CvWbc4Axq5F', 'function': {'arguments': '{\"query\":\"current weather in New York City\"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 23, 'prompt_tokens': 96, 'total_tokens': 119, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_45cf54deae', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-0658af11-b90b-407c-a6a5-6a3b4a9f61e0-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'current weather in New York City'}, 'id': 'call_wZ5w5uO733Cc4CvWbc4Axq5F', 'type': 'tool_call'}], usage_metadata={'input_tokens': 96, 'output_tokens': 23, 'total_tokens': 119, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}})\n",
+ "(('web_search_assistant:d08ae326-b6b2-1749-e8ea-4d308f22d819',), {'tools': {'messages': [ToolMessage(content='[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'New York\\', \\'region\\': \\'New York\\', \\'country\\': \\'United States of America\\', \\'lat\\': 40.714, \\'lon\\': -74.006, \\'tz_id\\': \\'America/New_York\\', \\'localtime_epoch\\': 1732021037, \\'localtime\\': \\'2024-11-19 07:57\\'}, \\'current\\': {\\'last_updated_epoch\\': 1732020300, \\'last_updated\\': \\'2024-11-19 07:45\\', \\'temp_c\\': 8.3, \\'temp_f\\': 46.9, \\'is_day\\': 1, \\'condition\\': {\\'text\\': \\'Sunny\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/day/113.png\\', \\'code\\': 1000}, \\'wind_mph\\': 7.2, \\'wind_kph\\': 11.5, \\'wind_degree\\': 332, \\'wind_dir\\': \\'NNW\\', \\'pressure_mb\\': 1016.0, \\'pressure_in\\': 29.99, \\'precip_mm\\': 0.0, \\'precip_in\\': 0.0, \\'humidity\\': 60, \\'cloud\\': 0, \\'feelslike_c\\': 6.3, \\'feelslike_f\\': 43.4, \\'windchill_c\\': 4.3, \\'windchill_f\\': 39.8, \\'heatindex_c\\': 7.0, \\'heatindex_f\\': 44.5, \\'dewpoint_c\\': 2.7, \\'dewpoint_f\\': 36.8, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 0.0, \\'gust_mph\\': 10.0, \\'gust_kph\\': 16.2}}\"}]', name='tavily_search_results_json', id='e955ebe9-631f-4dd1-b0a4-ee6a3caeba9f', tool_call_id='call_wZ5w5uO733Cc4CvWbc4Axq5F', artifact={'query': 'current weather in New York City', 'follow_up_questions': None, 'answer': None, 'images': [], 'results': [{'title': 'Weather in New York City', 'url': 'https://www.weatherapi.com/', 'content': \"{'location': {'name': 'New York', 'region': 'New York', 'country': 'United States of America', 'lat': 40.714, 'lon': -74.006, 'tz_id': 'America/New_York', 'localtime_epoch': 1732021037, 'localtime': '2024-11-19 07:57'}, 'current': {'last_updated_epoch': 1732020300, 'last_updated': '2024-11-19 07:45', 'temp_c': 8.3, 'temp_f': 46.9, 'is_day': 1, 'condition': {'text': 'Sunny', 'icon': '//cdn.weatherapi.com/weather/64x64/day/113.png', 'code': 1000}, 'wind_mph': 7.2, 'wind_kph': 11.5, 'wind_degree': 332, 'wind_dir': 'NNW', 'pressure_mb': 1016.0, 'pressure_in': 29.99, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 60, 'cloud': 0, 'feelslike_c': 6.3, 'feelslike_f': 43.4, 'windchill_c': 4.3, 'windchill_f': 39.8, 'heatindex_c': 7.0, 'heatindex_f': 44.5, 'dewpoint_c': 2.7, 'dewpoint_f': 36.8, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 0.0, 'gust_mph': 10.0, 'gust_kph': 16.2}}\", 'score': 0.9997275, 'raw_content': None}], 'response_time': 3.24})]}})\n",
+ "(('web_search_assistant:d08ae326-b6b2-1749-e8ea-4d308f22d819',), {'agent': {'messages': [AIMessage(content='The current weather in New York City is sunny with a temperature of 8.3°C (46.9°F). The wind is coming from the north-northwest at 7.2 mph (11.5 kph), and the humidity level is 60%. The weather feels slightly cooler at 6.3°C (43.4°F) due to the wind chill.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 79, 'prompt_tokens': 535, 'total_tokens': 614, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_159d8341cc', 'finish_reason': 'stop', 'logprobs': None}, id='run-43d275f1-aacb-44f4-bdd0-8233c3765699-0', usage_metadata={'input_tokens': 535, 'output_tokens': 79, 'total_tokens': 614, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}})\n",
+ "((), {'web_search_assistant': {'messages': [HumanMessage(content=\"what's the weather in nyc?\", additional_kwargs={}, response_metadata={}, id='756466d3-18ce-4b8e-b4fc-ee59932ce9f4'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_wZ5w5uO733Cc4CvWbc4Axq5F', 'function': {'arguments': '{\"query\":\"current weather in New York City\"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 23, 'prompt_tokens': 96, 'total_tokens': 119, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_45cf54deae', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-0658af11-b90b-407c-a6a5-6a3b4a9f61e0-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'current weather in New York City'}, 'id': 'call_wZ5w5uO733Cc4CvWbc4Axq5F', 'type': 'tool_call'}], usage_metadata={'input_tokens': 96, 'output_tokens': 23, 'total_tokens': 119, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content='[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'New York\\', \\'region\\': \\'New York\\', \\'country\\': \\'United States of America\\', \\'lat\\': 40.714, \\'lon\\': -74.006, \\'tz_id\\': \\'America/New_York\\', \\'localtime_epoch\\': 1732021037, \\'localtime\\': \\'2024-11-19 07:57\\'}, \\'current\\': {\\'last_updated_epoch\\': 1732020300, \\'last_updated\\': \\'2024-11-19 07:45\\', \\'temp_c\\': 8.3, \\'temp_f\\': 46.9, \\'is_day\\': 1, \\'condition\\': {\\'text\\': \\'Sunny\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/day/113.png\\', \\'code\\': 1000}, \\'wind_mph\\': 7.2, \\'wind_kph\\': 11.5, \\'wind_degree\\': 332, \\'wind_dir\\': \\'NNW\\', \\'pressure_mb\\': 1016.0, \\'pressure_in\\': 29.99, \\'precip_mm\\': 0.0, \\'precip_in\\': 0.0, \\'humidity\\': 60, \\'cloud\\': 0, \\'feelslike_c\\': 6.3, \\'feelslike_f\\': 43.4, \\'windchill_c\\': 4.3, \\'windchill_f\\': 39.8, \\'heatindex_c\\': 7.0, \\'heatindex_f\\': 44.5, \\'dewpoint_c\\': 2.7, \\'dewpoint_f\\': 36.8, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 0.0, \\'gust_mph\\': 10.0, \\'gust_kph\\': 16.2}}\"}]', name='tavily_search_results_json', id='e955ebe9-631f-4dd1-b0a4-ee6a3caeba9f', tool_call_id='call_wZ5w5uO733Cc4CvWbc4Axq5F', artifact={'query': 'current weather in New York City', 'follow_up_questions': None, 'answer': None, 'images': [], 'results': [{'title': 'Weather in New York City', 'url': 'https://www.weatherapi.com/', 'content': \"{'location': {'name': 'New York', 'region': 'New York', 'country': 'United States of America', 'lat': 40.714, 'lon': -74.006, 'tz_id': 'America/New_York', 'localtime_epoch': 1732021037, 'localtime': '2024-11-19 07:57'}, 'current': {'last_updated_epoch': 1732020300, 'last_updated': '2024-11-19 07:45', 'temp_c': 8.3, 'temp_f': 46.9, 'is_day': 1, 'condition': {'text': 'Sunny', 'icon': '//cdn.weatherapi.com/weather/64x64/day/113.png', 'code': 1000}, 'wind_mph': 7.2, 'wind_kph': 11.5, 'wind_degree': 332, 'wind_dir': 'NNW', 'pressure_mb': 1016.0, 'pressure_in': 29.99, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 60, 'cloud': 0, 'feelslike_c': 6.3, 'feelslike_f': 43.4, 'windchill_c': 4.3, 'windchill_f': 39.8, 'heatindex_c': 7.0, 'heatindex_f': 44.5, 'dewpoint_c': 2.7, 'dewpoint_f': 36.8, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 0.0, 'gust_mph': 10.0, 'gust_kph': 16.2}}\", 'score': 0.9997275, 'raw_content': None}], 'response_time': 3.24}), AIMessage(content='The current weather in New York City is sunny with a temperature of 8.3°C (46.9°F). The wind is coming from the north-northwest at 7.2 mph (11.5 kph), and the humidity level is 60%. The weather feels slightly cooler at 6.3°C (43.4°F) due to the wind chill.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 79, 'prompt_tokens': 535, 'total_tokens': 614, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_159d8341cc', 'finish_reason': 'stop', 'logprobs': None}, id='run-43d275f1-aacb-44f4-bdd0-8233c3765699-0', usage_metadata={'input_tokens': 535, 'output_tokens': 79, 'total_tokens': 614, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}})\n"
+ ]
+ }
+ ],
+ "source": [
+ "for chunk in graph.stream(\n",
+ " {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in nyc?\"}]},\n",
+ " subgraphs=True,\n",
+ "):\n",
+ " print(chunk)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ab2a7e63-a842-4e58-9c6a-e2203edec7b0",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.1"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/docs/how-tos/autogen-langgraph-platform.ipynb b/docs/docs/how-tos/autogen-langgraph-platform.ipynb
new file mode 100644
index 000000000..0d8cc4b19
--- /dev/null
+++ b/docs/docs/how-tos/autogen-langgraph-platform.ipynb
@@ -0,0 +1,173 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "8381b6e0-29a6-48c5-b451-5d2549351249",
+ "metadata": {},
+ "source": [
+ "# How to use LangGraph Platform to deploy CrewAI, AutoGen, and other frameworks\n",
+ "\n",
+ "[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) provides infrastructure for deploying agents. This integrates seamlessly with LangGraph, but can also work with other frameworks. The way to make this work is to wrap the agent in a single LangGraph node, and have that be the entire graph.\n",
+ "\n",
+ "Doing so will allow you to deploy to LangGraph Platform, and allows you to get a lot of the [benefits](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/). You get horizontally scalable infrastructure, a task queue to handle bursty operations, a persistence layer to power short term memory, and long term memory support.\n",
+ "\n",
+ "In this guide we show how to do this with an AutoGen agent, but this method should work for agents defined in other frameworks like CrewAI, LlamaIndex, and others as well."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1113cb16-b538-448c-924c-85731ce96ebd",
+ "metadata": {},
+ "source": [
+ "## Setup"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "f05993fa-9d03-4f45-bc13-0a8d87260d86",
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "# %pip install autogen langgraph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f4e0ca12-1714-4776-a30a-9527e519799b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# import os\n",
+ "# import getpass\n",
+ "\n",
+ "# os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1926bbc3-6b06-41e0-9604-860a2bbf8fa3",
+ "metadata": {},
+ "source": [
+ "## Define autogen agent\n",
+ "\n",
+ "Here we define our AutoGen agent. From https://github.com/microsoft/autogen/blob/0.2/notebook/agentchat_web_info.ipynb"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d4a14dc7-d565-4207-8788-525f85b9fb27",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import autogen\n",
+ "import os\n",
+ "\n",
+ "config_list = [{\"model\": \"gpt-4o\", \"api_key\": os.environ[\"OPENAI_API_KEY\"]}]\n",
+ "\n",
+ "llm_config = {\n",
+ " \"timeout\": 600,\n",
+ " \"cache_seed\": 42,\n",
+ " \"config_list\": config_list,\n",
+ " \"temperature\": 0,\n",
+ "}\n",
+ "\n",
+ "autogen_agent = autogen.AssistantAgent(\n",
+ " name=\"assistant\",\n",
+ " llm_config=llm_config,\n",
+ ")\n",
+ "\n",
+ "user_proxy = autogen.UserProxyAgent(\n",
+ " name=\"user_proxy\",\n",
+ " human_input_mode=\"NEVER\",\n",
+ " max_consecutive_auto_reply=10,\n",
+ " is_termination_msg=lambda x: x.get(\"content\", \"\").rstrip().endswith(\"TERMINATE\"),\n",
+ " code_execution_config={\n",
+ " \"work_dir\": \"web\",\n",
+ " \"use_docker\": False,\n",
+ " }, # Please set use_docker=True if docker is available to run the generated code. Using docker is safer than running the generated code directly.\n",
+ " llm_config=llm_config,\n",
+ " system_message=\"Reply TERMINATE if the task has been solved at full satisfaction. Otherwise, reply CONTINUE, or the reason why the task is not solved yet.\",\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b1170836-f23e-4e4c-ab83-ce791cd7fbd2",
+ "metadata": {},
+ "source": [
+ "## Wrap in LangGraph\n",
+ "\n",
+ "We now wrap the AutoGen agent in a single LangGraph node, and make that the entire graph.\n",
+ "The main thing this involves is defining an Input and Output schema for the node, which you would need to do if deploying this manually, so it's no extra work"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "7b417c16-ff4e-4d5c-a9a9-0aaeeef6ede5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langgraph.graph import StateGraph, MessagesState\n",
+ "\n",
+ "\n",
+ "def call_autogen_agent(state: MessagesState):\n",
+ " last_message = state[\"messages\"][-1]\n",
+ " response = user_proxy.initiate_chat(autogen_agent, message=last_message.content)\n",
+ " # get the final response from the agent\n",
+ " content = response.chat_history[-1][\"content\"]\n",
+ " return {\"messages\": {\"role\": \"assistant\", \"content\": content}}\n",
+ "\n",
+ "\n",
+ "graph = StateGraph(MessagesState)\n",
+ "graph.add_node(call_autogen_agent)\n",
+ "graph.set_entry_point(\"call_autogen_agent\")\n",
+ "graph = graph.compile()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f6a18377-ac29-478f-a76a-b213f1a3c85d",
+ "metadata": {},
+ "source": [
+ "## Deploy with LangGraph Platform\n",
+ "\n",
+ "You can now deploy this as you normally would with LangGraph Platform. See [these instructions](https://langchain-ai.github.io/langgraph/concepts/deployment_options/) for more details."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2b9c3ecb-0f36-4cfb-a10f-8a2e0ef6c730",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.1"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/docs/how-tos/configuration.ipynb b/docs/docs/how-tos/configuration.ipynb
index 7ea271681..589131b95 100644
--- a/docs/docs/how-tos/configuration.ipynb
+++ b/docs/docs/how-tos/configuration.ipynb
@@ -345,7 +345,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.4"
+ "version": "3.11.1"
}
},
"nbformat": 4,
diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md
index 92e7b526c..a224e0f31 100644
--- a/docs/docs/how-tos/index.md
+++ b/docs/docs/how-tos/index.md
@@ -103,6 +103,7 @@ These how-to guides show common patterns for tool calling with LangGraph:
- [How to force function calling agent to structure output](react-agent-structured-output.ipynb)
- [How to pass custom LangSmith run ID for graph runs](run-id-langsmith.ipynb)
- [How to return state before hitting recursion limit](return-when-recursion-limit-hits.ipynb)
+- [How to integrate LangGraph with AutoGen, CrewAI, and other frameworks](autogen-integration.ipynb)
### Prebuilt ReAct Agent
@@ -141,6 +142,7 @@ Learn how to set up your app for deployment to LangGraph Platform:
- [How to customize Dockerfile](../cloud/deployment/custom_docker.md)
- [How to test locally](../cloud/deployment/test_locally.md)
- [How to rebuild graph at runtime](../cloud/deployment/graph_rebuild.md)
+- [How to use LangGraph Platform to deploy CrewAI, AutoGen, and other frameworks](autogen-langgraph-platform.ipynb)
### Deployment
diff --git a/poetry.lock b/poetry.lock
index 64529d4aa..7d0162abb 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
@@ -390,6 +390,59 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
+[[package]]
+name = "autogen"
+version = "0.3.2"
+description = "A programming framework for agentic AI"
+optional = false
+python-versions = "<3.13,>=3.8"
+files = [
+ {file = "autogen-0.3.2-py3-none-any.whl", hash = "sha256:e37a9df0ad84cde3429ec63298b8e9eb4e6306a28eec2627171e14b9a61ea64d"},
+ {file = "autogen-0.3.2.tar.gz", hash = "sha256:9f8a1170ac2e5a1fc9efc3cfa6e23261dd014db97b17c8c416f97ee14951bc7b"},
+]
+
+[package.dependencies]
+diskcache = "*"
+docker = "*"
+flaml = "*"
+numpy = ">=1.17.0,<2"
+openai = ">=1.3"
+packaging = "*"
+pydantic = ">=1.10,<2.6.0 || >2.6.0,<3"
+python-dotenv = "*"
+termcolor = "*"
+tiktoken = "*"
+
+[package.extras]
+anthropic = ["anthropic (>=0.23.1)"]
+autobuild = ["chromadb", "huggingface-hub", "pysqlite3", "sentence-transformers"]
+bedrock = ["boto3 (>=1.34.149)"]
+blendsearch = ["flaml[blendsearch]"]
+cerebras = ["cerebras-cloud-sdk (>=1.0.0)"]
+cohere = ["cohere (>=5.5.8)"]
+cosmosdb = ["azure-cosmos (>=4.2.0)"]
+gemini = ["google-auth", "google-cloud-aiplatform", "google-generativeai (>=0.5,<1)", "pillow", "pydantic"]
+graph = ["matplotlib", "networkx"]
+graph-rag-falkor-db = ["graphrag-sdk"]
+groq = ["groq (>=0.9.0)"]
+jupyter-executor = ["ipykernel (>=6.29.0)", "jupyter-client (>=8.6.0)", "jupyter-kernel-gateway", "requests", "websocket-client"]
+lmm = ["pillow", "replicate"]
+long-context = ["llmlingua (<0.3)"]
+mathchat = ["pydantic (==1.10.9)", "sympy", "wolframalpha"]
+mistral = ["mistralai (>=1.0.1)"]
+ollama = ["fix-busted-json (>=0.0.18)", "ollama (>=0.3.3)"]
+redis = ["redis"]
+retrievechat = ["beautifulsoup4", "chromadb (==0.5.3)", "ipython", "markdownify", "protobuf (==4.25.3)", "pypdf", "sentence-transformers"]
+retrievechat-mongodb = ["beautifulsoup4", "chromadb (==0.5.3)", "ipython", "markdownify", "protobuf (==4.25.3)", "pymongo (>=4.0.0)", "pypdf", "sentence-transformers"]
+retrievechat-pgvector = ["beautifulsoup4", "chromadb (==0.5.3)", "ipython", "markdownify", "pgvector (>=0.2.5)", "protobuf (==4.25.3)", "psycopg (>=3.1.18)", "pypdf", "sentence-transformers"]
+retrievechat-qdrant = ["beautifulsoup4", "chromadb (==0.5.3)", "fastembed (>=0.3.1)", "ipython", "markdownify", "protobuf (==4.25.3)", "pypdf", "qdrant-client", "sentence-transformers"]
+teachable = ["chromadb"]
+test = ["ipykernel", "nbconvert", "nbformat", "pandas", "pre-commit", "pytest (>=6.1.1,<8)", "pytest-asyncio", "pytest-cov (>=5)"]
+together = ["together (>=1.2)"]
+types = ["ipykernel (>=6.29.0)", "jupyter-client (>=8.6.0)", "jupyter-kernel-gateway", "mypy (==1.9.0)", "pytest (>=6.1.1,<8)", "requests", "websocket-client"]
+websockets = ["websockets (>=12.0,<13)"]
+websurfer = ["beautifulsoup4", "markdownify", "pathvalidate", "pdfminer.six"]
+
[[package]]
name = "babel"
version = "2.16.0"
@@ -1135,6 +1188,17 @@ wrapt = ">=1.10,<2"
[package.extras]
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"]
+[[package]]
+name = "diskcache"
+version = "5.6.3"
+description = "Disk Cache -- Disk and file backed persistent cache."
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19"},
+ {file = "diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc"},
+]
+
[[package]]
name = "distro"
version = "1.9.0"
@@ -1166,6 +1230,28 @@ idna = ["idna (>=3.6)"]
trio = ["trio (>=0.23)"]
wmi = ["wmi (>=1.5.1)"]
+[[package]]
+name = "docker"
+version = "7.1.0"
+description = "A Python library for the Docker Engine API."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"},
+ {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"},
+]
+
+[package.dependencies]
+pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""}
+requests = ">=2.26.0"
+urllib3 = ">=1.26.0"
+
+[package.extras]
+dev = ["coverage (==7.2.7)", "pytest (==7.4.2)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.1.0)", "ruff (==0.1.8)"]
+docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"]
+ssh = ["paramiko (>=2.4.3)"]
+websockets = ["websocket-client (>=1.3.0)"]
+
[[package]]
name = "durationpy"
version = "0.7"
@@ -1271,6 +1357,43 @@ httpx-sse = "*"
Pillow = "*"
pydantic = "*"
+[[package]]
+name = "flaml"
+version = "2.3.2"
+description = "A fast library for automated machine learning and tuning"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "FLAML-2.3.2-py3-none-any.whl", hash = "sha256:1ee6e8e76bf1d741b4da41e2a2a8c0638b36d90b0f60aac323b5568f54dcb9e7"},
+ {file = "flaml-2.3.2.tar.gz", hash = "sha256:4a1ec289ddaec36850cfc66f6fb335b8521df49ea31f6adb54ea63a5cebb6865"},
+]
+
+[package.dependencies]
+NumPy = ">=1.17"
+
+[package.extras]
+autogen = ["diskcache", "openai (==0.27.8)", "termcolor"]
+automl = ["lightgbm (>=2.3.1)", "pandas (>=1.1.4)", "scikit-learn (>=1.0.0)", "scipy (>=1.4.1)", "xgboost (>=0.90,<3.0.0)"]
+autozero = ["packaging", "pandas", "scikit-learn"]
+azureml = ["azureml-mlflow"]
+benchmark = ["catboost (>=0.26)", "pandas (==1.1.4)", "psutil (==5.8.0)", "xgboost (==1.3.3)"]
+blendsearch = ["optuna (>=2.8.0,<=3.6.1)", "packaging"]
+catboost = ["catboost (>=0.26,<1.2)", "catboost (>=0.26,<=1.2.5)"]
+forecast = ["hcrystalball (==0.1.10)", "holidays (<0.14)", "prophet (>=1.0.1)", "pytorch-forecasting (>=0.9.0)", "pytorch-lightning (==1.9.0)", "statsmodels (>=0.12.2)", "tensorboardX (==2.6)"]
+hf = ["datasets", "nltk (<=3.8.1)", "rouge-score", "seqeval", "transformers[torch] (==4.26)"]
+mathchat = ["diskcache", "openai (==0.27.8)", "pydantic (==1.10.9)", "sympy", "termcolor", "wolframalpha"]
+nlp = ["datasets", "nltk (<=3.8.1)", "rouge-score", "seqeval", "transformers[torch] (==4.26)"]
+nni = ["nni"]
+notebook = ["jupyter"]
+openai = ["diskcache", "openai (==0.27.8)"]
+ray = ["ray[tune] (>=1.13,<2.0)"]
+retrievechat = ["chromadb", "diskcache", "openai (==0.27.8)", "sentence-transformers", "termcolor", "tiktoken"]
+spark = ["joblib (<=1.3.2)", "joblibspark (>=0.5.0)", "pyspark (>=3.2.0)"]
+synapse = ["joblibspark (>=0.5.0)", "optuna (>=2.8.0,<=3.6.1)", "pyspark (>=3.2.0)"]
+test = ["catboost (>=0.26)", "catboost (>=0.26,<1.2)", "coverage (>=5.3)", "dataclasses", "datasets", "dill", "hcrystalball (==0.1.10)", "ipykernel", "joblib (<=1.3.2)", "joblibspark (>=0.5.0)", "jupyter", "lightgbm (>=2.3.1)", "mlflow (==2.15.1)", "nbconvert", "nbformat", "nltk (<=3.8.1)", "openml", "optuna (>=2.8.0,<=3.6.1)", "packaging", "pandas (>=1.1.4)", "pandas (>=1.1.4,<2.0.0)", "pre-commit", "psutil (==5.8.0)", "pydantic (==1.10.9)", "pytest (>=6.1.1)", "pytorch-forecasting (>=0.9.0,<=0.10.1)", "pytorch-lightning (<1.9.1)", "requests (<2.29.0)", "rgf-python", "rouge-score", "scikit-learn (>=1.0.0)", "scipy (>=1.4.1)", "seqeval", "statsmodels (>=0.12.2)", "sympy", "tensorboardX (==2.6)", "thop", "torch", "torchvision", "transformers[torch] (==4.26)", "wolframalpha", "xgboost (>=0.90,<2.0.0)"]
+ts-forecast = ["hcrystalball (==0.1.10)", "holidays (<0.14)", "prophet (>=1.0.1)", "statsmodels (>=0.12.2)"]
+vw = ["scikit-learn", "vowpalwabbit (>=8.10.0,<9.0.0)"]
+
[[package]]
name = "flatbuffers"
version = "24.3.25"
@@ -2912,7 +3035,7 @@ langchain-core = ">=0.3.0,<0.4.0"
[[package]]
name = "langgraph"
-version = "0.2.34"
+version = "0.2.52"
description = "Building stateful, multi-actor applications with LLMs"
optional = false
python-versions = ">=3.9.0,<4.0"
@@ -2920,8 +3043,9 @@ files = []
develop = true
[package.dependencies]
-langchain-core = ">=0.2.39,<0.4"
-langgraph-checkpoint = "^2.0.0"
+langchain-core = ">=0.2.43,<0.4.0,!=0.3.0,!=0.3.1,!=0.3.2,!=0.3.3,!=0.3.4,!=0.3.5,!=0.3.6,!=0.3.7,!=0.3.8,!=0.3.9,!=0.3.10,!=0.3.11,!=0.3.12,!=0.3.13,!=0.3.14"
+langgraph-checkpoint = "^2.0.4"
+langgraph-sdk = "^0.1.32"
[package.source]
type = "directory"
@@ -2929,7 +3053,7 @@ url = "libs/langgraph"
[[package]]
name = "langgraph-checkpoint"
-version = "2.0.1"
+version = "2.0.5"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -2946,7 +3070,7 @@ url = "libs/checkpoint"
[[package]]
name = "langgraph-checkpoint-postgres"
-version = "2.0.1"
+version = "2.0.3"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -2954,7 +3078,7 @@ files = []
develop = true
[package.dependencies]
-langgraph-checkpoint = "^2.0.0"
+langgraph-checkpoint = "^2.0.2"
orjson = ">=3.10.1"
psycopg = "^3.0.0"
psycopg-pool = "^3.0.0"
@@ -2965,7 +3089,7 @@ url = "libs/checkpoint-postgres"
[[package]]
name = "langgraph-checkpoint-sqlite"
-version = "2.0.0"
+version = "2.0.1"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
optional = false
python-versions = "^3.9.0"
@@ -2974,7 +3098,7 @@ develop = true
[package.dependencies]
aiosqlite = "^0.20.0"
-langgraph-checkpoint = "^2.0.0"
+langgraph-checkpoint = "^2.0.2"
[package.source]
type = "directory"
@@ -2982,7 +3106,7 @@ url = "libs/checkpoint-sqlite"
[[package]]
name = "langgraph-sdk"
-version = "0.1.32"
+version = "0.1.36"
description = "SDK for interacting with LangGraph API"
optional = false
python-versions = "^3.9.0,<4.0"
@@ -4974,6 +5098,7 @@ description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs
optional = false
python-versions = ">=3.8"
files = [
+ {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
{file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
]
@@ -4984,6 +5109,7 @@ description = "A collection of ASN.1-based protocols modules"
optional = false
python-versions = ">=3.8"
files = [
+ {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"},
{file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"},
]
@@ -6043,6 +6169,11 @@ files = [
{file = "scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd"},
{file = "scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:394397841449853c2290a32050382edaec3da89e35b3e03d6cc966aebc6a8ae6"},
{file = "scikit_learn-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:57cc1786cfd6bd118220a92ede80270132aa353647684efa385a74244a41e3b1"},
+ {file = "scikit_learn-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5"},
+ {file = "scikit_learn-1.5.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908"},
+ {file = "scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3"},
+ {file = "scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12"},
+ {file = "scikit_learn-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f"},
{file = "scikit_learn-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:757c7d514ddb00ae249832fe87100d9c73c6ea91423802872d9e74970a0e40b9"},
{file = "scikit_learn-1.5.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:52788f48b5d8bca5c0736c175fa6bdaab2ef00a8f536cda698db61bd89c551c1"},
{file = "scikit_learn-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:643964678f4b5fbdc95cbf8aec638acc7aa70f5f79ee2cdad1eec3df4ba6ead8"},
@@ -6362,6 +6493,20 @@ files = [
doc = ["reno", "sphinx"]
test = ["pytest", "tornado (>=4.5)", "typeguard"]
+[[package]]
+name = "termcolor"
+version = "2.5.0"
+description = "ANSI color formatting for output in terminal"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"},
+ {file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"},
+]
+
+[package.extras]
+tests = ["pytest", "pytest-cov"]
+
[[package]]
name = "terminado"
version = "0.18.1"
@@ -7331,4 +7476,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
-content-hash = "738e69cf406b140217cc8c3c0f2ccb5c2027d4701bcf2a347ba4b107f700ab2a"
+content-hash = "776ee42630769f08e3896338f18ec81830166695d32d2208dc31dedb22d3b22d"
diff --git a/pyproject.toml b/pyproject.toml
index dc50e167f..31fe17172 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,6 +54,7 @@ motor = "^3.5.1"
grandalf = "^0.8"
pyppeteer = "^2.0.0"
networkx = "^3.3"
+autogen = { version = "^0.3.0", python = "<3.13,>=3.8" }
[tool.poetry.group.test]
optional = true
From b1779cf348d36a4087ce438802effeb407f519a6 Mon Sep 17 00:00:00 2001
From: Vadym Barda
Date: Tue, 19 Nov 2024 11:57:26 -0500
Subject: [PATCH 005/149] docs: update autogen docs (#2470)
---
docs/_scripts/prepare_notebooks_for_ci.py | 2 ++
docs/docs/how-tos/autogen-integration.ipynb | 26 +++++++++----------
.../how-tos/autogen-langgraph-platform.ipynb | 24 ++++++++---------
3 files changed, 25 insertions(+), 27 deletions(-)
diff --git a/docs/_scripts/prepare_notebooks_for_ci.py b/docs/_scripts/prepare_notebooks_for_ci.py
index eee19acab..dfa1e1c70 100644
--- a/docs/_scripts/prepare_notebooks_for_ci.py
+++ b/docs/_scripts/prepare_notebooks_for_ci.py
@@ -36,6 +36,8 @@ NOTEBOOKS_NO_EXECUTION = [
"docs/docs/tutorials/rag/langgraph_self_rag_local.ipynb",
# this loads a massive dataset from gcp
"docs/docs/tutorials/usaco/usaco.ipynb",
+ # TODO: figure out why autogen notebook is not runnable (they are just hanging. possible due to code execution?)
+ "docs/docs/how-tos/autogen-integration.ipynb",
# TODO: need to update these notebooks to make sure they are runnable in CI
"docs/docs/tutorials/storm/storm.ipynb", # issues only when running with VCR
"docs/docs/tutorials/lats/lats.ipynb", # issues only when running with VCR
diff --git a/docs/docs/how-tos/autogen-integration.ipynb b/docs/docs/how-tos/autogen-integration.ipynb
index 1673014d4..207a3b6f3 100644
--- a/docs/docs/how-tos/autogen-integration.ipynb
+++ b/docs/docs/how-tos/autogen-integration.ipynb
@@ -29,7 +29,7 @@
"metadata": {},
"outputs": [],
"source": [
- "# %pip install autogen bs4 langgraph langchain-openai langchain-community"
+ "%pip install autogen bs4 langgraph langchain-openai langchain-community"
]
},
{
@@ -39,11 +39,17 @@
"metadata": {},
"outputs": [],
"source": [
- "# import os\n",
- "# import getpass\n",
+ "import getpass\n",
+ "import os\n",
"\n",
- "# os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()\n",
- "# os.environ[\"TAVILY_API_KEY\"] = getpass.getpass()"
+ "\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\")\n",
+ "_set_env(\"TAVILY_API_KEY\")"
]
},
{
@@ -300,14 +306,6 @@
"):\n",
" print(chunk)"
]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "ab2a7e63-a842-4e58-9c6a-e2203edec7b0",
- "metadata": {},
- "outputs": [],
- "source": []
}
],
"metadata": {
@@ -326,7 +324,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.1"
+ "version": "3.12.3"
}
},
"nbformat": 4,
diff --git a/docs/docs/how-tos/autogen-langgraph-platform.ipynb b/docs/docs/how-tos/autogen-langgraph-platform.ipynb
index 0d8cc4b19..29bd24ba5 100644
--- a/docs/docs/how-tos/autogen-langgraph-platform.ipynb
+++ b/docs/docs/how-tos/autogen-langgraph-platform.ipynb
@@ -31,7 +31,7 @@
},
"outputs": [],
"source": [
- "# %pip install autogen langgraph"
+ "%pip install autogen langgraph"
]
},
{
@@ -41,10 +41,16 @@
"metadata": {},
"outputs": [],
"source": [
- "# import os\n",
- "# import getpass\n",
+ "import getpass\n",
+ "import os\n",
"\n",
- "# os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()"
+ "\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\")"
]
},
{
@@ -139,14 +145,6 @@
"\n",
"You can now deploy this as you normally would with LangGraph Platform. See [these instructions](https://langchain-ai.github.io/langgraph/concepts/deployment_options/) for more details."
]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "2b9c3ecb-0f36-4cfb-a10f-8a2e0ef6c730",
- "metadata": {},
- "outputs": [],
- "source": []
}
],
"metadata": {
@@ -165,7 +163,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.1"
+ "version": "3.12.3"
}
},
"nbformat": 4,
From b3fa43e4a6308e5a65cade60eadd571d9211d649 Mon Sep 17 00:00:00 2001
From: bracesproul
Date: Tue, 19 Nov 2024 09:23:47 -0800
Subject: [PATCH 006/149] fix(sdk-js): remove trailing slash from url
---
libs/sdk-js/src/client.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/libs/sdk-js/src/client.ts b/libs/sdk-js/src/client.ts
index e154c87cf..caaf80dd4 100644
--- a/libs/sdk-js/src/client.ts
+++ b/libs/sdk-js/src/client.ts
@@ -62,7 +62,8 @@ class BaseClient {
// default limit being capped by Chrome
// https://github.com/nodejs/undici/issues/1373
- this.apiUrl = config?.apiUrl || "http://localhost:8123";
+ // Regex to remove trailing slash, if present
+ this.apiUrl = config?.apiUrl?.replace(/\/$/, "") || "http://localhost:8123";
this.defaultHeaders = config?.defaultHeaders || {};
if (config?.apiKey != null) {
this.defaultHeaders["X-Api-Key"] = config.apiKey;
From 153245145e44f9b5d3be0378a143bc9864eaba58 Mon Sep 17 00:00:00 2001
From: bracesproul
Date: Tue, 19 Nov 2024 11:32:22 -0800
Subject: [PATCH 007/149] (sdk-js): Release 0.0.26
---
libs/sdk-js/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/sdk-js/package.json b/libs/sdk-js/package.json
index bc1744108..bc37a9d4e 100644
--- a/libs/sdk-js/package.json
+++ b/libs/sdk-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
- "version": "0.0.25",
+ "version": "0.0.26",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
From e3e63c70c93d127b632b67538c76d0e4d6b400cf Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Tue, 19 Nov 2024 14:54:08 -0500
Subject: [PATCH 008/149] docs: how-to guide language changes (#2462)
---
docs/docs/how-tos/index.md | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md
index a224e0f31..1419fc1f2 100644
--- a/docs/docs/how-tos/index.md
+++ b/docs/docs/how-tos/index.md
@@ -151,6 +151,7 @@ LangGraph applications can be deployed using LangGraph Cloud, which provides a r
- [How to deploy to LangGraph cloud](../cloud/deployment/cloud.md)
- [How to deploy to a self-hosted environment](./deploy-self-hosted.md)
- [How to interact with the deployment using RemoteGraph](./use-remote-graph.md)
+
### Assistants
[Assistants](../concepts/assistants.md) is a configured instance of a template.
@@ -165,7 +166,7 @@ LangGraph applications can be deployed using LangGraph Cloud, which provides a r
### Runs
-LangGraph Cloud supports multiple types of runs besides streaming runs.
+LangGraph Platform supports multiple types of runs besides streaming runs.
- [How to run an agent in the background](../cloud/how-tos/background_run.md)
- [How to run multiple agents in the same thread](../cloud/how-tos/same-thread.md)
@@ -185,7 +186,7 @@ Streaming the results of your LLM application is vital for ensuring a good user
### Human-in-the-loop
-When creating complex graphs, leaving every decision up to the LLM can be dangerous, especially when the decisions involve invoking certain tools or accessing specific documents. To remedy this, LangGraph allows you to insert human-in-the-loop behavior to ensure your graph does not have undesired outcomes. Read more about the different ways you can add human-in-the-loop capabilities to your LangGraph Cloud projects in these how-to guides:
+When designing complex graphs, relying entirely on the LLM for decision-making can be risky, particularly when it involves tools that interact with files, APIs, or databases. These interactions may lead to unintended data access or modifications, depending on the use case. To mitigate these risks, LangGraph allows you to integrate human-in-the-loop behavior, ensuring your LLM applications operate as intended without undesirable outcomes.
- [How to add a breakpoint](../cloud/how-tos/human_in_the_loop_breakpoint.md)
- [How to wait for user input](../cloud/how-tos/human_in_the_loop_user_input.md)
@@ -195,7 +196,7 @@ When creating complex graphs, leaving every decision up to the LLM can be danger
### Double-texting
-Graph execution can take a while, and sometimes users may change their mind about the input they wanted to send before their original input has finished running. For example, a user might notice a typo in their original request and will edit the prompt and resend it. Deciding what to do in these cases is important for ensuring a smooth user experience and preventing your graphs from behaving in unexpected ways. The following how-to guides provide information on the various options LangGraph Cloud gives you for dealing with double-texting:
+Graph execution can take a while, and sometimes users may change their mind about the input they wanted to send before their original input has finished running. For example, a user might notice a typo in their original request and will edit the prompt and resend it. Deciding what to do in these cases is important for ensuring a smooth user experience and preventing your graphs from behaving in unexpected ways.
- [How to use the interrupt option](../cloud/how-tos/interrupt_concurrent.md)
- [How to use the rollback option](../cloud/how-tos/rollback_concurrent.md)
From 7e8eef88ca21c71d9f4194fda7100a25061dd9eb Mon Sep 17 00:00:00 2001
From: Vadym Barda
Date: Wed, 20 Nov 2024 14:36:42 -0500
Subject: [PATCH 009/149] docs: small fix for tutorial (#2487)
---
docs/docs/tutorials/multi_agent/agent_supervisor.ipynb | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/docs/docs/tutorials/multi_agent/agent_supervisor.ipynb b/docs/docs/tutorials/multi_agent/agent_supervisor.ipynb
index d7952e990..c1b8d1231 100644
--- a/docs/docs/tutorials/multi_agent/agent_supervisor.ipynb
+++ b/docs/docs/tutorials/multi_agent/agent_supervisor.ipynb
@@ -158,7 +158,6 @@
"from typing_extensions import TypedDict\n",
"\n",
"from langchain_anthropic import ChatAnthropic\n",
- "from langgraph.graph import MessagesState\n",
"\n",
"members = [\"researcher\", \"coder\"]\n",
"# Our team supervisor is an LLM node. It just picks the next agent to process\n",
@@ -253,7 +252,7 @@
" }\n",
"\n",
"\n",
- "builder = StateGraph(MessagesState)\n",
+ "builder = StateGraph(AgentState)\n",
"builder.add_edge(START, \"supervisor\")\n",
"builder.add_node(\"supervisor\", supervisor_node)\n",
"builder.add_node(\"researcher\", research_node)\n",
From 9766068896a7e5910bd8d92bc4c3ecee971531d4 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 20 Nov 2024 15:20:53 -0800
Subject: [PATCH 010/149] lib: For subgraphs / stream modes call stream.put as
a callback in the original event loop
- This is asynchronous, so we shouldn't use for regular writes to the output stream (ie those from PregelLoop)
- For writes from subgraphs / nodes this is fine to use, as we make no guarantees about when those show up anyway
---
libs/langgraph/langgraph/pregel/__init__.py | 15 ++++--
libs/langgraph/tests/fake_chat.py | 55 ++++++++++++++++++++-
2 files changed, 64 insertions(+), 6 deletions(-)
diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py
index 59d2736a0..36150f829 100644
--- a/libs/langgraph/langgraph/pregel/__init__.py
+++ b/libs/langgraph/langgraph/pregel/__init__.py
@@ -1806,12 +1806,16 @@ class Pregel(PregelProtocol):
# set up messages stream mode
if "messages" in stream_modes:
run_manager.inheritable_handlers.append(
- StreamMessagesHandler(stream.put_nowait)
+ StreamMessagesHandler(
+ partial(aioloop.call_soon_threadsafe, stream.put_nowait)
+ )
)
# set up custom stream mode
if "custom" in stream_modes:
- config[CONF][CONFIG_KEY_STREAM_WRITER] = lambda c: stream.put_nowait(
- ((), "custom", c)
+ config[CONF][CONFIG_KEY_STREAM_WRITER] = (
+ lambda c: aioloop.call_soon_threadsafe(
+ stream.put_nowait, ((), "custom", c)
+ )
)
async with AsyncPregelLoop(
input,
@@ -1838,7 +1842,10 @@ class Pregel(PregelProtocol):
)
# enable subgraph streaming
if subgraphs:
- loop.config[CONF][CONFIG_KEY_STREAM] = loop.stream
+ loop.config[CONF][CONFIG_KEY_STREAM] = StreamProtocol(
+ partial(aioloop.call_soon_threadsafe, stream.put_nowait),
+ stream_modes,
+ )
# enable concurrent streaming
if subgraphs or "messages" in stream_modes or "custom" in stream_modes:
diff --git a/libs/langgraph/tests/fake_chat.py b/libs/langgraph/tests/fake_chat.py
index c2a6b9b9e..d4a76ef7c 100644
--- a/libs/langgraph/tests/fake_chat.py
+++ b/libs/langgraph/tests/fake_chat.py
@@ -1,7 +1,10 @@
import re
-from typing import Any, Iterator, List, Optional, cast
+from typing import Any, AsyncIterator, Iterator, List, Optional, cast
-from langchain_core.callbacks import CallbackManagerForLLMRun
+from langchain_core.callbacks import (
+ AsyncCallbackManagerForLLMRun,
+ CallbackManagerForLLMRun,
+)
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage
from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
@@ -84,3 +87,51 @@ class FakeChatModel(GenericFakeChatModel):
if run_manager:
run_manager.on_llm_new_token("", chunk=chunk)
yield chunk
+
+ async def _astream(
+ self,
+ messages: List[BaseMessage],
+ stop: Optional[List[str]] = None,
+ run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
+ **kwargs: Any,
+ ) -> AsyncIterator[ChatGenerationChunk]:
+ """Stream the output of the model."""
+ chat_result = self._generate(
+ messages, stop=stop, run_manager=run_manager, **kwargs
+ )
+ if not isinstance(chat_result, ChatResult):
+ raise ValueError(
+ f"Expected generate to return a ChatResult, "
+ f"but got {type(chat_result)} instead."
+ )
+
+ message = chat_result.generations[0].message
+
+ if not isinstance(message, AIMessage):
+ raise ValueError(
+ f"Expected invoke to return an AIMessage, "
+ f"but got {type(message)} instead."
+ )
+
+ content = message.content
+
+ if content:
+ # Use a regular expression to split on whitespace with a capture group
+ # so that we can preserve the whitespace in the output.
+ assert isinstance(content, str)
+ content_chunks = cast(list[str], re.split(r"(\s)", content))
+
+ for token in content_chunks:
+ chunk = ChatGenerationChunk(
+ message=AIMessageChunk(content=token, id=message.id)
+ )
+ if run_manager:
+ run_manager.on_llm_new_token(token, chunk=chunk)
+ yield chunk
+ else:
+ args = message.__dict__
+ args.pop("type")
+ chunk = ChatGenerationChunk(message=AIMessageChunk(**args))
+ if run_manager:
+ await run_manager.on_llm_new_token("", chunk=chunk)
+ yield chunk
From a5706627732de5b35398c4765121107870e76caf Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 20 Nov 2024 15:43:46 -0800
Subject: [PATCH 011/149] Lint
---
libs/langgraph/langgraph/pregel/__init__.py | 12 +++++++-----
libs/langgraph/langgraph/pregel/loop.py | 2 +-
libs/langgraph/langgraph/pregel/messages.py | 2 +-
libs/langgraph/langgraph/utils/config.py | 4 ++--
4 files changed, 11 insertions(+), 9 deletions(-)
diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py
index 36150f829..62506142d 100644
--- a/libs/langgraph/langgraph/pregel/__init__.py
+++ b/libs/langgraph/langgraph/pregel/__init__.py
@@ -107,6 +107,7 @@ from langgraph.types import (
Checkpointer,
LoopProtocol,
StateSnapshot,
+ StreamChunk,
StreamMode,
)
from langgraph.utils.config import (
@@ -1752,6 +1753,10 @@ class Pregel(PregelProtocol):
stream = AsyncQueue()
aioloop = asyncio.get_running_loop()
+ stream_put = cast(
+ Callable[[StreamChunk], None],
+ partial(aioloop.call_soon_threadsafe, stream.put_nowait),
+ )
def output() -> Iterator:
while True:
@@ -1806,9 +1811,7 @@ class Pregel(PregelProtocol):
# set up messages stream mode
if "messages" in stream_modes:
run_manager.inheritable_handlers.append(
- StreamMessagesHandler(
- partial(aioloop.call_soon_threadsafe, stream.put_nowait)
- )
+ StreamMessagesHandler(stream_put)
)
# set up custom stream mode
if "custom" in stream_modes:
@@ -1843,8 +1846,7 @@ class Pregel(PregelProtocol):
# enable subgraph streaming
if subgraphs:
loop.config[CONF][CONFIG_KEY_STREAM] = StreamProtocol(
- partial(aioloop.call_soon_threadsafe, stream.put_nowait),
- stream_modes,
+ stream_put, stream_modes
)
# enable concurrent streaming
if subgraphs or "messages" in stream_modes or "custom" in stream_modes:
diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py
index 6a9b6a95e..2a68b00f2 100644
--- a/libs/langgraph/langgraph/pregel/loop.py
+++ b/libs/langgraph/langgraph/pregel/loop.py
@@ -110,13 +110,13 @@ from langgraph.types import (
Command,
LoopProtocol,
PregelExecutableTask,
+ StreamChunk,
StreamProtocol,
)
from langgraph.utils.config import patch_configurable
V = TypeVar("V")
P = ParamSpec("P")
-StreamChunk = tuple[tuple[str, ...], str, Any]
INPUT_DONE = object()
INPUT_RESUMING = object()
diff --git a/libs/langgraph/langgraph/pregel/messages.py b/libs/langgraph/langgraph/pregel/messages.py
index 2c31de279..989f116dc 100644
--- a/libs/langgraph/langgraph/pregel/messages.py
+++ b/libs/langgraph/langgraph/pregel/messages.py
@@ -18,7 +18,7 @@ from langchain_core.outputs import ChatGenerationChunk, LLMResult
from langchain_core.tracers._streaming import T, _StreamingCallbackHandler
from langgraph.constants import NS_SEP, TAG_HIDDEN, TAG_NOSTREAM
-from langgraph.pregel.loop import StreamChunk
+from langgraph.types import StreamChunk
Meta = tuple[tuple[str, ...], dict[str, Any]]
diff --git a/libs/langgraph/langgraph/utils/config.py b/libs/langgraph/langgraph/utils/config.py
index adb26dd89..5bff9e848 100644
--- a/libs/langgraph/langgraph/utils/config.py
+++ b/libs/langgraph/langgraph/utils/config.py
@@ -1,7 +1,7 @@
import asyncio
import sys
from collections import ChainMap
-from typing import Any, Optional, Sequence
+from typing import Any, Optional, Sequence, cast
from langchain_core.callbacks import (
AsyncCallbackManager,
@@ -281,7 +281,7 @@ def ensure_config(*configs: Optional[RunnableConfig]) -> RunnableConfig:
for k, v in config.items():
if v is not None and k in CONFIG_KEYS:
if k == CONF:
- empty[k] = v.copy() # type: ignore[literal-required]
+ empty[k] = cast(dict, v).copy()
else:
empty[k] = v # type: ignore[literal-required]
for k, v in config.items():
From 3c0de2691407de00bc81ddd66ed2920051fae002 Mon Sep 17 00:00:00 2001
From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 20 Nov 2024 16:21:33 -0800
Subject: [PATCH 012/149] Accept 3.13 in build
---
libs/cli/langgraph_cli/cli.py | 2 +-
libs/cli/langgraph_cli/config.py | 29 ++++++++++++++++++++----
libs/cli/tests/unit_tests/test_config.py | 19 ++++++++++++++++
3 files changed, 44 insertions(+), 6 deletions(-)
diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py
index 4460f3f41..2fe9ef834 100644
--- a/libs/cli/langgraph_cli/cli.py
+++ b/libs/cli/langgraph_cli/cli.py
@@ -45,7 +45,7 @@ OPT_CONFIG = click.option(
- "graphs": mapping from graph ID to path where the compiled graph is defined, i.e. ./your_package/your_file.py:variable, where
"variable" is an instance of langgraph.graph.graph.CompiledGraph
- "env": (optional) path to .env file or a mapping from environment variable to its value
- - "python_version": (optional) 3.11 or 3.12. Defaults to 3.11
+ - "python_version": (optional) 3.11, 3.12, or 3.13. Defaults to 3.11
- "pip_config_file": (optional) path to pip config file
- "dockerfile_lines": (optional) array of additional lines to add to Dockerfile following the import from parent image
diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py
index 16473083f..549fbffbc 100644
--- a/libs/cli/langgraph_cli/config.py
+++ b/libs/cli/langgraph_cli/config.py
@@ -17,6 +17,18 @@ class Config(TypedDict):
env: Union[dict[str, str], str]
+MIN_PYTHON_VERSION = "3.11"
+
+
+def _parse_version(version_str: str) -> tuple[int, int]:
+ """Parse a version string into a tuple of (major, minor)."""
+ try:
+ major, minor = map(int, version_str.split("."))
+ return (major, minor)
+ except ValueError:
+ raise click.UsageError(f"Invalid version format: {version_str}") from None
+
+
def validate_config(config: Config) -> Config:
config = (
{
@@ -44,14 +56,21 @@ def validate_config(config: Config) -> Config:
)
if config.get("python_version"):
- if config["python_version"] not in (
- "3.11",
- "3.12",
+ pyversion = config["python_version"]
+ if not pyversion.count(".") == 1 or not all(
+ part.isdigit() for part in pyversion.split(".")
):
raise click.UsageError(
- f"Unsupported Python version: {config['python_version']}. "
- "Supported versions are 3.11 and 3.12."
+ f"Invalid Python version format: {pyversion}. "
+ "Use 'major.minor' format (e.g., '3.11'). "
+ "Patch version cannot be specified."
)
+ if _parse_version(pyversion) < _parse_version(MIN_PYTHON_VERSION):
+ raise click.UsageError(
+ f"Python version {pyversion} is not supported. "
+ f"Minimum required version is {MIN_PYTHON_VERSION}."
+ )
+
if not config["dependencies"]:
raise click.UsageError(
"No dependencies found in config. "
diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py
index cff16ead6..d12c660cd 100644
--- a/libs/cli/tests/unit_tests/test_config.py
+++ b/libs/cli/tests/unit_tests/test_config.py
@@ -42,6 +42,9 @@ def test_validate_config():
}
actual_config = validate_config(expected_config)
assert actual_config == expected_config
+ expected_config["python_version"] = "3.13"
+ actual_config = validate_config(expected_config)
+ assert actual_config == expected_config
# check wrong python version raises
with pytest.raises(click.UsageError):
@@ -61,6 +64,22 @@ def test_validate_config():
with pytest.raises(click.UsageError):
validate_config({"python_version": "3.9", "dependencies": ["."]})
+ with pytest.raises(click.UsageError) as exc_info:
+ validate_config({"python_version": "3.11.0"})
+ assert "Invalid Python version format" in str(exc_info.value)
+
+ with pytest.raises(click.UsageError) as exc_info:
+ validate_config({"python_version": "3"})
+ assert "Invalid Python version format" in str(exc_info.value)
+
+ with pytest.raises(click.UsageError) as exc_info:
+ validate_config({"python_version": "abc.def"})
+ assert "Invalid Python version format" in str(exc_info.value)
+
+ with pytest.raises(click.UsageError) as exc_info:
+ validate_config({"python_version": "3.10"})
+ assert "Minimum required version" in str(exc_info.value)
+
# config_to_docker
def test_config_to_docker_simple():
From 588373c2d5a7fbe4713a16caa87f8239f26390fa Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 20 Nov 2024 17:13:42 -0800
Subject: [PATCH 013/149] 0.2.53
---
libs/langgraph/pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml
index d92effd90..6db05f90d 100644
--- a/libs/langgraph/pyproject.toml
+++ b/libs/langgraph/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
-version = "0.2.52"
+version = "0.2.53"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
From a93377643694322ce8cc7b4e3ea674e2382730f3 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 20 Nov 2024 17:24:34 -0800
Subject: [PATCH 014/149] [CLI] Validate node version (#2489)
---
.github/scripts/run_langgraph_cli_test.py | 3 +-
libs/cli/langgraph_cli/cli.py | 27 +++-----
libs/cli/langgraph_cli/config.py | 77 ++++++++++++++++++++---
libs/cli/pyproject.toml | 2 +-
libs/cli/tests/unit_tests/test_config.py | 73 ++++++++++++++++++++-
5 files changed, 153 insertions(+), 29 deletions(-)
diff --git a/.github/scripts/run_langgraph_cli_test.py b/.github/scripts/run_langgraph_cli_test.py
index a6024778b..478a215ab 100644
--- a/.github/scripts/run_langgraph_cli_test.py
+++ b/.github/scripts/run_langgraph_cli_test.py
@@ -22,8 +22,7 @@ def test(
# check docker available
capabilities = langgraph_cli.docker.check_capabilities(runner)
# open config
- with open(config) as f:
- config_json = langgraph_cli.config.validate_config(json.load(f))
+ config_json = langgraph_cli.config.validate_config_file(config)
set("Running...")
args = [
diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py
index 2fe9ef834..4de7568ea 100644
--- a/libs/cli/langgraph_cli/cli.py
+++ b/libs/cli/langgraph_cli/cli.py
@@ -1,4 +1,3 @@
-import json
import pathlib
import shutil
import sys
@@ -354,8 +353,7 @@ def build(
with Runner() as runner, Progress(message="Pulling...") as set:
if shutil.which("docker") is None:
raise click.UsageError("Docker not installed") from None
- with open(config) as f:
- config_json = langgraph_cli.config.validate_config(json.load(f))
+ config_json = langgraph_cli.config.validate_config_file(config)
_build(
runner, set, config, config_json, base_image, pull, tag, docker_build_args
)
@@ -435,8 +433,7 @@ tests
def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -> None:
save_path = pathlib.Path(save_path).absolute()
secho(f"🔍 Validating configuration at path: {config}", fg="yellow")
- with open(config, encoding="utf-8") as f:
- config_json = langgraph_cli.config.validate_config(json.load(f))
+ config_json = langgraph_cli.config.validate_config_file(config)
secho("✅ Configuration validated!", fg="green")
secho(f"📝 Generating Dockerfile at {save_path}", fg="yellow")
@@ -575,7 +572,7 @@ def dev(
host: str,
port: int,
no_reload: bool,
- config: str,
+ config: pathlib.Path,
n_jobs_per_worker: Optional[int],
no_browser: bool,
debug_port: Optional[int],
@@ -601,12 +598,9 @@ def dev(
"Please ensure langgraph-cli is installed with the 'inmem' extra: pip install -U \"langgraph-cli[inmem]\""
) from None
- import json
+ config_json = langgraph_cli.config.validate_config_file(config)
- with open(config, encoding="utf-8") as f:
- config_data = json.load(f)
-
- graphs = config_data.get("graphs", {})
+ graphs = config_json.get("graphs", {})
run_server(
host,
port,
@@ -674,8 +668,7 @@ def prepare(
debugger_base_url: Optional[str] = None,
postgres_uri: Optional[str] = None,
):
- with open(config_path) as f:
- config = langgraph_cli.config.validate_config(json.load(f))
+ config_json = langgraph_cli.config.validate_config_file(config_path)
# pull latest images
if pull:
runner.run(
@@ -683,9 +676,9 @@ def prepare(
"docker",
"pull",
(
- f"langchain/langgraphjs-api:{config['node_version']}"
- if config.get("node_version")
- else f"langchain/langgraph-api:{config['python_version']}"
+ f"langchain/langgraphjs-api:{config_json['node_version']}"
+ if config_json.get("node_version")
+ else f"langchain/langgraph-api:{config_json['python_version']}"
),
verbose=verbose,
)
@@ -694,7 +687,7 @@ def prepare(
args, stdin = prepare_args_and_stdin(
capabilities=capabilities,
config_path=config_path,
- config=config,
+ config=config_json,
docker_compose=docker_compose,
port=port,
watch=watch,
diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py
index 549fbffbc..95b72e0cd 100644
--- a/libs/cli/langgraph_cli/config.py
+++ b/libs/cli/langgraph_cli/config.py
@@ -6,6 +6,9 @@ from typing import NamedTuple, Optional, TypedDict, Union
import click
+MIN_NODE_VERSION = "20"
+MIN_PYTHON_VERSION = "3.11"
+
class Config(TypedDict):
python_version: str
@@ -17,9 +20,6 @@ class Config(TypedDict):
env: Union[dict[str, str], str]
-MIN_PYTHON_VERSION = "3.11"
-
-
def _parse_version(version_str: str) -> tuple[int, int]:
"""Parse a version string into a tuple of (major, minor)."""
try:
@@ -29,6 +29,19 @@ def _parse_version(version_str: str) -> tuple[int, int]:
raise click.UsageError(f"Invalid version format: {version_str}") from None
+def _parse_node_version(version_str: str) -> int:
+ """Parse a Node.js version string into a major version number."""
+ try:
+ if "." in version_str:
+ raise ValueError("Node.js version must be major version only")
+ return int(version_str)
+ except ValueError:
+ raise click.UsageError(
+ f"Invalid Node.js version format: {version_str}. "
+ "Use major version only (e.g., '20')."
+ ) from None
+
+
def validate_config(config: Config) -> Config:
config = (
{
@@ -49,11 +62,17 @@ def validate_config(config: Config) -> Config:
)
if config.get("node_version"):
- if config["node_version"] not in ("20",):
- raise click.UsageError(
- f"Unsupported Node.js version: {config['node_version']}. "
- "Currently only `node_version: \"20\"` is supported."
- )
+ node_version = config["node_version"]
+ try:
+ major = _parse_node_version(node_version)
+ min_major = _parse_node_version(MIN_NODE_VERSION)
+ if major < min_major:
+ raise click.UsageError(
+ f"Node.js version {node_version} is not supported. "
+ f"Minimum required version is {MIN_NODE_VERSION}."
+ )
+ except ValueError as e:
+ raise click.UsageError(str(e)) from None
if config.get("python_version"):
pyversion = config["python_version"]
@@ -85,6 +104,48 @@ def validate_config(config: Config) -> Config:
return config
+def validate_config_file(config_path: pathlib.Path) -> Config:
+ with open(config_path) as f:
+ config = json.load(f)
+ validated = validate_config(config)
+ # Enforce the package.json doesn't enforce an
+ # incompatible Node.js version
+ if validated.get("node_version"):
+ package_json_path = config_path.parent / "package.json"
+ if package_json_path.is_file():
+ try:
+ with open(package_json_path) as f:
+ package_json = json.load(f)
+ if "engines" in package_json:
+ engines = package_json["engines"]
+ if any(engine != "node" for engine in engines.keys()):
+ raise click.UsageError(
+ "Only 'node' engine is supported in package.json engines."
+ f" Got engines: {list(engines.keys())}"
+ )
+ if engines:
+ node_version = engines["node"]
+ try:
+ major = _parse_node_version(node_version)
+ min_major = _parse_node_version(MIN_NODE_VERSION)
+ if major < min_major:
+ raise click.UsageError(
+ f"Node.js version in package.json engines must be >= {MIN_NODE_VERSION} "
+ f"(major version only), got '{node_version}'. Minor/patch versions "
+ "(like '20.x.y') are not supported to prevent deployment issues "
+ "when new Node.js versions are released."
+ )
+ except ValueError as e:
+ raise click.UsageError(str(e)) from None
+
+ except json.JSONDecodeError:
+ raise click.UsageError(
+ "Invalid package.json found in langgraph "
+ f"config directory {package_json_path}: file is not valid JSON"
+ ) from None
+ return validated
+
+
class LocalDeps(NamedTuple):
pip_reqs: list[tuple[pathlib.Path, str]]
real_pkgs: dict[pathlib.Path, str]
diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml
index 217e3762e..bc64904ff 100644
--- a/libs/cli/pyproject.toml
+++ b/libs/cli/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
-version = "0.1.55"
+version = "0.1.56"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py
index d12c660cd..c1632a8b3 100644
--- a/libs/cli/tests/unit_tests/test_config.py
+++ b/libs/cli/tests/unit_tests/test_config.py
@@ -1,10 +1,17 @@
+import json
import os
import pathlib
+import tempfile
import click
import pytest
-from langgraph_cli.config import config_to_compose, config_to_docker, validate_config
+from langgraph_cli.config import (
+ config_to_compose,
+ config_to_docker,
+ validate_config,
+ validate_config_file,
+)
from langgraph_cli.util import clean_empty_lines
PATH_TO_CONFIG = pathlib.Path(__file__).parent / "test_config.json"
@@ -81,6 +88,70 @@ def test_validate_config():
assert "Minimum required version" in str(exc_info.value)
+def test_validate_config_file():
+ with tempfile.TemporaryDirectory() as tmpdir:
+ tmpdir_path = pathlib.Path(tmpdir)
+
+ config_path = tmpdir_path / "langgraph.json"
+
+ node_config = {"node_version": "20", "graphs": {"agent": "./agent.js:graph"}}
+ with open(config_path, "w") as f:
+ json.dump(node_config, f)
+
+ validate_config_file(config_path)
+
+ package_json = {"name": "test", "engines": {"node": "20"}}
+ with open(tmpdir_path / "package.json", "w") as f:
+ json.dump(package_json, f)
+ validate_config_file(config_path)
+
+ package_json["engines"]["node"] = "20.18"
+ with open(tmpdir_path / "package.json", "w") as f:
+ json.dump(package_json, f)
+ with pytest.raises(click.UsageError, match="Use major version only"):
+ validate_config_file(config_path)
+
+ package_json["engines"] = {"node": "18"}
+ with open(tmpdir_path / "package.json", "w") as f:
+ json.dump(package_json, f)
+ with pytest.raises(click.UsageError, match="must be >= 20"):
+ validate_config_file(config_path)
+
+ package_json["engines"] = {"node": "20", "deno": "1.0"}
+ with open(tmpdir_path / "package.json", "w") as f:
+ json.dump(package_json, f)
+ with pytest.raises(click.UsageError, match="Only 'node' engine is supported"):
+ validate_config_file(config_path)
+
+ with open(tmpdir_path / "package.json", "w") as f:
+ f.write("{invalid json")
+ with pytest.raises(click.UsageError, match="Invalid package.json"):
+ validate_config_file(config_path)
+
+ python_config = {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ }
+ with open(config_path, "w") as f:
+ json.dump(python_config, f)
+
+ validate_config_file(config_path)
+
+ for package_content in [
+ {"name": "test"},
+ {"engines": {"node": "18"}},
+ {"engines": {"node": "20", "deno": "1.0"}},
+ "{invalid json",
+ ]:
+ with open(tmpdir_path / "package.json", "w") as f:
+ if isinstance(package_content, dict):
+ json.dump(package_content, f)
+ else:
+ f.write(package_content)
+ validate_config_file(config_path)
+
+
# config_to_docker
def test_config_to_docker_simple():
graphs = {"agent": "./agent.py:graph"}
From b977045679cb912e2e45567838c71f7a74160d04 Mon Sep 17 00:00:00 2001
From: vbarda
Date: Wed, 20 Nov 2024 21:24:50 -0500
Subject: [PATCH 015/149] langgraph: fix error message on invalid update
---
libs/langgraph/langgraph/graph/state.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py
index c581d2259..cce742911 100644
--- a/libs/langgraph/langgraph/graph/state.py
+++ b/libs/langgraph/langgraph/graph/state.py
@@ -627,13 +627,16 @@ class CompiledStateGraph(CompiledGraph):
else:
return input
+ # to avoid name collision below
+ node_key = key
+
def _get_state_key(input: Union[None, dict, Any], *, key: str) -> Any:
if input is None:
return SKIP_WRITE
elif isinstance(input, dict):
if all(k not in output_keys for k in input):
raise InvalidUpdateError(
- f"Expected node {key} to update at least one of {output_keys}, got {input}"
+ f"Expected node {node_key} to update at least one of {output_keys}, got {input}"
)
return input.get(key, SKIP_WRITE)
elif isinstance(input, Command):
From 54d848913f5c530703fd33049415c91c3a7dc003 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Thu, 21 Nov 2024 07:49:59 -0800
Subject: [PATCH 016/149] [CLI] Update Inmem Version (#2500)
---
libs/cli/poetry.lock | 1128 ++++++++++++++++++---------------------
libs/cli/pyproject.toml | 6 +-
2 files changed, 534 insertions(+), 600 deletions(-)
diff --git a/libs/cli/poetry.lock b/libs/cli/poetry.lock
index f2e6ebd23..5f88d1e84 100644
--- a/libs/cli/poetry.lock
+++ b/libs/cli/poetry.lock
@@ -23,10 +23,8 @@ files = [
]
[package.dependencies]
-exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
idna = ">=2.8"
sniffio = ">=1.1"
-typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
[package.extras]
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
@@ -44,6 +42,85 @@ files = [
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
]
+[[package]]
+name = "cffi"
+version = "1.17.1"
+description = "Foreign Function Interface for Python calling C code."
+optional = true
+python-versions = ">=3.8"
+files = [
+ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
+ {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
+ {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
+ {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
+ {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
+ {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
+ {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
+ {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
+ {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
+ {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
+ {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
+ {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
+ {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
+ {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
+ {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
+ {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
+ {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
+ {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
+ {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
+ {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
+ {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
+ {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
+ {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
+ {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
+ {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
+ {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
+ {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
+ {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
+ {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
+ {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
+ {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
+ {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
+ {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
+ {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
+ {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
+ {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
+ {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
+ {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
+ {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
+ {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
+ {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
+ {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
+ {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
+ {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
+ {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
+ {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
+ {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
+ {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
+ {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
+ {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
+ {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
+ {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
+ {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
+ {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
+ {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
+ {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
+ {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
+ {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
+ {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
+ {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
+ {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
+ {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
+ {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
+ {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
+ {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
+ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
+ {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
+]
+
+[package.dependencies]
+pycparser = "*"
+
[[package]]
name = "charset-normalizer"
version = "3.4.0"
@@ -174,13 +251,13 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "codespell"
-version = "2.2.6"
+version = "2.3.0"
description = "Codespell"
optional = false
python-versions = ">=3.8"
files = [
- {file = "codespell-2.2.6-py3-none-any.whl", hash = "sha256:9ee9a3e5df0990604013ac2a9f22fa8e57669c827124a2e961fe8a1da4cacc07"},
- {file = "codespell-2.2.6.tar.gz", hash = "sha256:a8c65d8eb3faa03deabab6b3bbe798bea72e1799c7e9e955d57eca4096abcff9"},
+ {file = "codespell-2.3.0-py3-none-any.whl", hash = "sha256:a9c7cef2501c9cfede2110fd6d4e5e62296920efe9abfb84648df866e47f58d1"},
+ {file = "codespell-2.3.0.tar.gz", hash = "sha256:360c7d10f75e65f67bad720af7007e1060a5d395670ec11a7ed1fed9dd17471f"},
]
[package.extras]
@@ -201,19 +278,53 @@ files = [
]
[[package]]
-name = "croniter"
-version = "5.0.1"
-description = "croniter provides iteration for datetime object with cron like format"
+name = "cryptography"
+version = "43.0.3"
+description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = true
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.6"
+python-versions = ">=3.7"
files = [
- {file = "croniter-5.0.1-py2.py3-none-any.whl", hash = "sha256:eb28439742291f6c10b181df1a5ecf421208b1fc62ef44501daec1780a0b09e9"},
- {file = "croniter-5.0.1.tar.gz", hash = "sha256:7d9b1ef25b10eece48fdf29d8ac52f9b6252abff983ac614ade4f3276294019e"},
+ {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"},
+ {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"},
+ {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"},
+ {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"},
+ {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"},
+ {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"},
+ {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"},
+ {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"},
+ {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"},
+ {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"},
+ {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"},
+ {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"},
+ {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"},
+ {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"},
+ {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"},
+ {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"},
+ {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"},
+ {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"},
+ {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"},
+ {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"},
+ {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"},
+ {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"},
+ {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"},
+ {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"},
+ {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"},
+ {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"},
+ {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"},
]
[package.dependencies]
-python-dateutil = "*"
-pytz = ">2021.1"
+cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
+
+[package.extras]
+docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
+docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
+nox = ["nox"]
+pep8test = ["check-sdist", "click", "mypy", "ruff"]
+sdist = ["build"]
+ssh = ["bcrypt (>=3.1.5)"]
+test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
+test-randomorder = ["pytest-randomly"]
[[package]]
name = "docopt"
@@ -227,13 +338,13 @@ files = [
[[package]]
name = "exceptiongroup"
-version = "1.2.0"
+version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
files = [
- {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
- {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
+ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
+ {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
]
[package.extras]
@@ -271,61 +382,6 @@ http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
trio = ["trio (>=0.22.0,<1.0)"]
-[[package]]
-name = "httptools"
-version = "0.6.4"
-description = "A collection of framework independent HTTP protocol utils."
-optional = true
-python-versions = ">=3.8.0"
-files = [
- {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"},
- {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"},
- {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"},
- {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"},
- {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"},
- {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"},
- {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"},
- {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"},
- {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"},
- {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"},
- {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"},
- {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"},
- {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"},
- {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"},
- {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"},
- {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"},
- {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"},
- {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"},
- {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"},
- {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"},
- {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"},
- {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"},
- {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"},
- {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"},
- {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"},
- {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"},
- {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"},
- {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"},
- {file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"},
- {file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"},
- {file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"},
- {file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"},
- {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"},
- {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"},
- {file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"},
- {file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"},
- {file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"},
- {file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"},
- {file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"},
- {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"},
- {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"},
- {file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"},
- {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"},
-]
-
-[package.extras]
-test = ["Cython (>=0.29.24)"]
-
[[package]]
name = "httpx"
version = "0.27.2"
@@ -414,54 +470,54 @@ files = [
[[package]]
name = "jsonschema-rs"
-version = "0.26.1"
+version = "0.25.1"
description = "A high-performance JSON Schema validator for Python"
optional = true
python-versions = ">=3.8"
files = [
- {file = "jsonschema_rs-0.26.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5552075a161fd79e25dadd5a15f3708eb2a896e55e22b8622bb092250f3fb677"},
- {file = "jsonschema_rs-0.26.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:619fdc2ed6b771fd884ce717d2d0526e58b7c3ad5852d20dfd2387023b031a7b"},
- {file = "jsonschema_rs-0.26.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2ce2c6ac1f09418e87dafe35c43c21211be8ea60c1bb6c5c2e4c0f71da821bf9"},
- {file = "jsonschema_rs-0.26.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc06f403f15004f891d8dc5bad48b4e8b281c4fcf26c15489183a109e4cca478"},
- {file = "jsonschema_rs-0.26.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85457113acde9982d5be7017046294cc8b0ebcaa56b7b063bab32112396ba2bc"},
- {file = "jsonschema_rs-0.26.1-cp310-none-win32.whl", hash = "sha256:451d40262ce8dc529ec9d3dceb41073b70977b923cf5b4d9f96692f80f6230ad"},
- {file = "jsonschema_rs-0.26.1-cp310-none-win_amd64.whl", hash = "sha256:5c6ddccebae684603a56f647fbf4a348a3cd98841972affa9543220da7da14ac"},
- {file = "jsonschema_rs-0.26.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8fb8df6eba2df02347d3e8db88a8350d54a54c7722a3426dd2bf7ca06b7526e2"},
- {file = "jsonschema_rs-0.26.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f20a9aa908027782e005ec5557c90aec723aca98d05d7380183a49595ec1b907"},
- {file = "jsonschema_rs-0.26.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d85c8c05fe1dd8803df2819e1928fcf71e6c465ce22905841a4745cab6e88e22"},
- {file = "jsonschema_rs-0.26.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a593b80cc43eb3d80a908323b18972cbe9287e1e5fa4195f2e876dccb48e8d8"},
- {file = "jsonschema_rs-0.26.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af6a64a9c29b730d9dcb4b212a8dd32bb492aaeb5bac584a2bac7afb21b64ca0"},
- {file = "jsonschema_rs-0.26.1-cp311-none-win32.whl", hash = "sha256:63c494df42f8dd5a96ab89b0f485f0cf315d2a74d9f74d06ae441ca8f0b37aa6"},
- {file = "jsonschema_rs-0.26.1-cp311-none-win_amd64.whl", hash = "sha256:2a4650b87fc8e575716544fe59736c4bf122061dbe38e5c2804685728f922dd5"},
- {file = "jsonschema_rs-0.26.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e5fedc9c06832ce2e115fb29c8f812392eabc031f44b8fc998a17fd272b85078"},
- {file = "jsonschema_rs-0.26.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fea71b79b2bf33449571f042530f38c4fd955c221fb8344b1a174b3986a24ef1"},
- {file = "jsonschema_rs-0.26.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9582c29abdd4dd8f90f4ed5ccb4e3fca5e5d80faaf6ee7dd8b9cdfca474a1790"},
- {file = "jsonschema_rs-0.26.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58435b0f316d2a3d4ac8241367980fa66c8ad72783228531d5640e7be891845a"},
- {file = "jsonschema_rs-0.26.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69bf6258f9529ef468cd3b307fdec7216e0233a48b9fde9c6faa946e4024f684"},
- {file = "jsonschema_rs-0.26.1-cp312-none-win32.whl", hash = "sha256:2da5da5456b78488ab17aeef25cbd4ed74962568db4029aa74d024992e74b87a"},
- {file = "jsonschema_rs-0.26.1-cp312-none-win_amd64.whl", hash = "sha256:55ba30c2fcb8e13a9762c5028c779ef77c7e53e19b3ebdfbcd1bead17103dcd7"},
- {file = "jsonschema_rs-0.26.1-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:548be7473f0803a671aae7090c563e051ba1d6dec38d11e516fa1a75f397b9bf"},
- {file = "jsonschema_rs-0.26.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:874ed3bc52ca04490a53fca14f8c0f14c72522ca7359c7ee7e26facd605a4c2c"},
- {file = "jsonschema_rs-0.26.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3e761b451ba8869ff02a563f7302532937f9c6047ce8f85b205c5708a2b34c72"},
- {file = "jsonschema_rs-0.26.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cc08bcccf87fb22cbf70bd73ee181148829c2a21c62bff53e44967e7bce4f3b"},
- {file = "jsonschema_rs-0.26.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc08d65d799a0c877d15b2547a5aa4035a8c11fce4bc12894c4d5d8a64345e17"},
- {file = "jsonschema_rs-0.26.1-cp313-none-win32.whl", hash = "sha256:02b54cb3c6152686d031f8265e1241667a0813ed21073ada0b0f351b193d58ac"},
- {file = "jsonschema_rs-0.26.1-cp313-none-win_amd64.whl", hash = "sha256:fdde9a2bc9c3fba12d8addfce162af8415fadaf6842f733093ec88d8b3ba924f"},
- {file = "jsonschema_rs-0.26.1-cp38-cp38-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:25a65fd50a7a5c81a59deaedf70fd363635f807492be172bc91e3478f41e07b8"},
- {file = "jsonschema_rs-0.26.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b56931b4d4660725cb728fae2c3f4342c04c668e9c050e3ce39b8d1ff783b5a2"},
- {file = "jsonschema_rs-0.26.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8e25d79fc78bfdeed4892be365bf10218dc501996952b54a39985992f3a63021"},
- {file = "jsonschema_rs-0.26.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4708b364ba5308e7104b1991e0eae5092ffb46894817e74646faf0a1ac2eebd"},
- {file = "jsonschema_rs-0.26.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f9552eb5196d1a23ba9ad94c6d2138f4c769ff85a06a398c5bd56ad33c2dc34"},
- {file = "jsonschema_rs-0.26.1-cp38-none-win32.whl", hash = "sha256:0e296388e3a5f82464936c7d530565bfb17560af0bc60da13f7e06f620941d38"},
- {file = "jsonschema_rs-0.26.1-cp38-none-win_amd64.whl", hash = "sha256:000a15187ac8db38013ccdecee3f0dced1942787792a4d35a35d4840e605f842"},
- {file = "jsonschema_rs-0.26.1-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:651ef97aa4033675d45d68aea2276cee2f46c5f38ee938d6cc6a46901b4e9cab"},
- {file = "jsonschema_rs-0.26.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:f59f4bdbba1f74813f1888ecd80828c2a7547b11a5cdcd100e0065eb7a0995f7"},
- {file = "jsonschema_rs-0.26.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3893361d2a97e9d695ac43ff01e3665b5d2fa0e845cd8cfba15d36783a1e6fd4"},
- {file = "jsonschema_rs-0.26.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34b22b0b49565ea0fdf8e776a714c506c51134ca82f5d9b68d17f778075757fa"},
- {file = "jsonschema_rs-0.26.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba1bd5b5cbdc516032eb00f5e1f3b96f2b9fa47eb84fb09055000d0ea27fd61a"},
- {file = "jsonschema_rs-0.26.1-cp39-none-win32.whl", hash = "sha256:8596c26c1a73e58c87004f95892d61118121377d8b608b052c0a194778585b24"},
- {file = "jsonschema_rs-0.26.1-cp39-none-win_amd64.whl", hash = "sha256:4d94e995b870046bb08eaab9f1220dfeda5a1414d6a7016ef72b731349ea148d"},
- {file = "jsonschema_rs-0.26.1.tar.gz", hash = "sha256:c711aedbd1e6911b12780e2a937241946f270590e27de495aa482bc8ce49aaa4"},
+ {file = "jsonschema_rs-0.25.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0dc49a465a02f97a8c747e852bc82322dd56ff7d66b3058c9656e15b8a5e381c"},
+ {file = "jsonschema_rs-0.25.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cf196d06dea3af58a23e6a03e363140ee307fc80fc77d8c71068df3de1bca588"},
+ {file = "jsonschema_rs-0.25.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f7bc2cac3891b75c301090effba458b6e7108d568aa2aeadb5695d7788991fa3"},
+ {file = "jsonschema_rs-0.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bc130df50b530c57524c7edc29808700d51304f94477833fb0062e0f3df1dfb"},
+ {file = "jsonschema_rs-0.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dace81a4652029493a19e24454bb5f821acdacf65da4f6472bbdc6751c45b79"},
+ {file = "jsonschema_rs-0.25.1-cp310-none-win32.whl", hash = "sha256:dc9771bbe5672f89a974b7302f5e4421559a8d0903eab2c38510dd61bd526086"},
+ {file = "jsonschema_rs-0.25.1-cp310-none-win_amd64.whl", hash = "sha256:7be237cc251c0fe9fe196adf4a7a8402339bbf58ef8710ab76b9e4b845fc94bd"},
+ {file = "jsonschema_rs-0.25.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:df22e50adba344efc018322305386357bf30c4b6a9b48349f565ff410d1eb78e"},
+ {file = "jsonschema_rs-0.25.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:db9fb2086a8a64e40e95e0f6bb0a91ea387419d0d239fc59d468586702286a89"},
+ {file = "jsonschema_rs-0.25.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a168559e9f1fff42d9a2a78c46143fda7ee58a90b85a7d11140dfe16e609e92b"},
+ {file = "jsonschema_rs-0.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e4f659487071009749077b4f5ee69300772fa321f92fa831e71fd4218839958"},
+ {file = "jsonschema_rs-0.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b1e9f7c05c8c9656bb98ecc93a0b91408a6e73063956dea7452cd64277f3b9f"},
+ {file = "jsonschema_rs-0.25.1-cp311-none-win32.whl", hash = "sha256:406a18dafac01b7799dd86b1b05bf14e8628d1f8bc0a1a9c187458f246c2f6cc"},
+ {file = "jsonschema_rs-0.25.1-cp311-none-win_amd64.whl", hash = "sha256:3546274ae6e11fcc1b058cdd763803c4db24e49bac125076bc4fab0ab61d4786"},
+ {file = "jsonschema_rs-0.25.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:cfaed89ef79cc972d11c50889fa02e8265aa5b6b6826c40749b956888712909d"},
+ {file = "jsonschema_rs-0.25.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:87432343bcbd91475af28c765caa0bbde534bbfb6fac1245735a5a1dd6b522a4"},
+ {file = "jsonschema_rs-0.25.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e69c9faed321b7cfbeb37e98e385a0392ad8d25896fd3def56afd080e56e294c"},
+ {file = "jsonschema_rs-0.25.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f004018ffb7fbffc8f1dbeb1bda44f322c924e007bb6966ab3466bdffbc06e62"},
+ {file = "jsonschema_rs-0.25.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b05e2f6bf86ced9d912d72329a19d39a86fcc49a85e31b923456d982733eaa7f"},
+ {file = "jsonschema_rs-0.25.1-cp312-none-win32.whl", hash = "sha256:b3d2ea6217e3618ba587d374b89f4660de59293b0a2fa43278131bc2cb339f57"},
+ {file = "jsonschema_rs-0.25.1-cp312-none-win_amd64.whl", hash = "sha256:6ceeaec97e77d14d6355aec9e55b2356d6376fdf94f6fed10c29cadc8e51fdf6"},
+ {file = "jsonschema_rs-0.25.1-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4696a580f54855cda68c05bbd7c25328eadca34b6a31b36495961235c511148d"},
+ {file = "jsonschema_rs-0.25.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a2d9844d70e5a481a1c363b8b02a3862cc1dbc3b14551a8060436e53243d5447"},
+ {file = "jsonschema_rs-0.25.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd168893519bae7c09fb7226f4ac4b6e73bc1ce9397c3846870cf980135762e8"},
+ {file = "jsonschema_rs-0.25.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e6627f4ecf1cb12765734f39a536bb4a062e35c9db756d52581b4d045489025"},
+ {file = "jsonschema_rs-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4784b0c1a0596595e0ea482d6656ccd33d7f14fbf32456e76fead0a67c6a7ec5"},
+ {file = "jsonschema_rs-0.25.1-cp313-none-win32.whl", hash = "sha256:7e6984721dbaaffc6a32ba15b4988df56e4069c9f867370a8a3c48a69a311ac3"},
+ {file = "jsonschema_rs-0.25.1-cp313-none-win_amd64.whl", hash = "sha256:62234e768a1cc57690602711e37f2b936b0f081dcd34a9c2b3e20ba0dafcbceb"},
+ {file = "jsonschema_rs-0.25.1-cp38-cp38-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:346a46f25d974b4ae1d36ac3c02677a699b46404b019a688ddabef40840019fc"},
+ {file = "jsonschema_rs-0.25.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:32f4908bc5c958a0a94a25260f5be59fba0062a60a40b61f0faedf12eed82063"},
+ {file = "jsonschema_rs-0.25.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ce090f2038f1a01836adf9f821f7e8c82073710c7a55b002a2e7a1625c3f3954"},
+ {file = "jsonschema_rs-0.25.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4ef0f3b0760ab151c35f5c8e090cdb201fb81eae2cf5ea1771f4d827d0cfbb1"},
+ {file = "jsonschema_rs-0.25.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c732bd71da96d6600ae550401e7997fb470f73f95af8defefe4fcbc5e4e9043"},
+ {file = "jsonschema_rs-0.25.1-cp38-none-win32.whl", hash = "sha256:b9ea15dfa5c47b1658e77c3a04e7c38b66e6a617617e1b9f5dba53b67712da1e"},
+ {file = "jsonschema_rs-0.25.1-cp38-none-win_amd64.whl", hash = "sha256:0d8438bea4c09994973ac60c645f4735808329908f4b9421f07d7f2ddf8ac860"},
+ {file = "jsonschema_rs-0.25.1-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6c9ea0b69fab4a27e0c8c0edbee24db2da44d2a97657469ee30a2b980b0b445c"},
+ {file = "jsonschema_rs-0.25.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:9de3946a1cd66daae805578ad5cbf21ff4d590d7206048f0eb2999aab056a78a"},
+ {file = "jsonschema_rs-0.25.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3bd2ae54066e840cb5595d135b40c9458a0155e482998e00e922ea6b5ab57a24"},
+ {file = "jsonschema_rs-0.25.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb7e2a6777e78a699d09e5c6e84318e97a8e80727939fcde3d0faff7f9da85f3"},
+ {file = "jsonschema_rs-0.25.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a87babb64a93bad6bfbf2ddad0e33cab6c3f395e9572fe027387c7767e9741c"},
+ {file = "jsonschema_rs-0.25.1-cp39-none-win32.whl", hash = "sha256:570dffc76f0b4e9fa93cf4678a6b07d6fa446f1e5557525703160a94c0ab7968"},
+ {file = "jsonschema_rs-0.25.1-cp39-none-win_amd64.whl", hash = "sha256:9117e105f979f11f55ae940949a544e7e1a304b56ae65f535fa82cc8af4dc2e1"},
+ {file = "jsonschema_rs-0.25.1.tar.gz", hash = "sha256:f2fe71253bb0315061a5025b9336fd49660bca4094b04948f53e3acfd4197a64"},
]
[package.extras]
@@ -493,13 +549,13 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph"
-version = "0.2.52"
+version = "0.2.53"
description = "Building stateful, multi-actor applications with LLMs"
optional = true
python-versions = "<4.0,>=3.9.0"
files = [
- {file = "langgraph-0.2.52-py3-none-any.whl", hash = "sha256:4cec56629b833a4f61678d5395719f7a096b16438bf521c48744e9d0579f3559"},
- {file = "langgraph-0.2.52.tar.gz", hash = "sha256:d28b58787b44b880fcdd9479a61a35cc9c273d007eb756ee468fb2f5f72c7738"},
+ {file = "langgraph-0.2.53-py3-none-any.whl", hash = "sha256:b34b67d0a12ae0ba6f03af97ad0f744bc609bd0328e8b734618cc039985cfdea"},
+ {file = "langgraph-0.2.53.tar.gz", hash = "sha256:b83232a04f2b536cbeac542f9ad7e0265f41ac6b7c6706ba8e031e0e80cb13a6"},
]
[package.dependencies]
@@ -508,33 +564,32 @@ langgraph-checkpoint = ">=2.0.4,<3.0.0"
langgraph-sdk = ">=0.1.32,<0.2.0"
[[package]]
-name = "langgraph-api-inmem"
-version = "0.0.3"
+name = "langgraph-api"
+version = "0.0.2"
description = ""
optional = true
-python-versions = "<4.0.0,>=3.9.0"
+python-versions = "<4.0,>=3.11.0"
files = [
- {file = "langgraph_api_inmem-0.0.3-py3-none-any.whl", hash = "sha256:161f76dd916048c59ae2007bea3ab3e066479aeeaef3d348e815908c18af82d0"},
- {file = "langgraph_api_inmem-0.0.3.tar.gz", hash = "sha256:9ca6a4065877967028909fb1b5b9085503d7b0021067a43e663e55c865ff7e2c"},
+ {file = "langgraph_api-0.0.2-py3-none-any.whl", hash = "sha256:7a30fb21987572eacc93dd1c69c2155c17957afed71dde18d6f47992b3124d65"},
+ {file = "langgraph_api-0.0.2.tar.gz", hash = "sha256:b751afca96cb6db67fe2f48e798ada27a4df068f0df86b36d2b8eee52344bbf0"},
]
[package.dependencies]
-croniter = ">=1.0.1"
-httptools = ">=0.5.0"
-httpx = ">=0.25.0"
-jsonschema-rs = ">=0.20.0"
+cryptography = ">=43.0.3,<44.0.0"
+httpx = ">=0.27.0"
+jsonschema-rs = ">=0.25.0,<0.26.0"
langchain-core = ">=0.2.38,<0.4.0"
-langgraph = ">=0.2.52,<0.3.0"
+langgraph = ">=0.2.52"
langgraph-checkpoint = ">=2.0.5,<3.0"
-langsmith = ">=0.1.63"
-orjson = ">=3.9.7"
-pycurl = ">=7.45.0"
-sse-starlette = ">=2.1.0"
-structlog = ">=23.1.0"
-tenacity = ">=8.0.0"
-tornado = ">=6.4.0"
+langsmith = ">=0.1.63,<0.2.0"
+orjson = ">=3.10.1"
+pyjwt = ">=2.9.0,<3.0.0"
+sse-starlette = ">=2.1.0,<3.0.0"
+starlette = ">=0.38.6"
+structlog = ">=24.4.0,<25.0.0"
+tenacity = ">=8.3.0,<9.0.0"
uvicorn = ">=0.26.0"
-uvloop = ">=0.18.0"
+watchfiles = ">=0.13"
[[package]]
name = "langgraph-checkpoint"
@@ -569,18 +624,18 @@ orjson = ">=3.10.1"
[[package]]
name = "langsmith"
-version = "0.1.143"
+version = "0.1.144"
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
optional = true
python-versions = "<4.0,>=3.8.1"
files = [
- {file = "langsmith-0.1.143-py3-none-any.whl", hash = "sha256:ba0d827269e9b03a90fababe41fa3e4e3f833300b95add10184f7e67167dde6f"},
- {file = "langsmith-0.1.143.tar.gz", hash = "sha256:4c5159e5cd84b3f8499433009e72d2076dd2daf6c044ac8a3611b30d0d0161c5"},
+ {file = "langsmith-0.1.144-py3-none-any.whl", hash = "sha256:08ffb975bff2e82fc6f5428837c64c074ea25102d08a25e256361a80812c6100"},
+ {file = "langsmith-0.1.144.tar.gz", hash = "sha256:b621f358d5a33441d7b5e7264c376bf4ea82bfc62d7e41aafc0f8094e3bd6369"},
]
[package.dependencies]
httpx = ">=0.23.0,<1"
-orjson = ">=3.9.14,<4.0.0"
+orjson = {version = ">=3.9.14,<4.0.0", markers = "platform_python_implementation != \"PyPy\""}
pydantic = [
{version = ">=1,<3", markers = "python_full_version < \"3.12.4\""},
{version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""},
@@ -663,47 +718,53 @@ files = [
[[package]]
name = "mypy"
-version = "1.10.0"
+version = "1.13.0"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
files = [
- {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"},
- {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"},
- {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"},
- {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"},
- {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"},
- {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"},
- {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"},
- {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"},
- {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"},
- {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"},
- {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"},
- {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"},
- {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"},
- {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"},
- {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"},
- {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"},
- {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"},
- {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"},
- {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"},
- {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"},
- {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"},
- {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"},
- {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"},
- {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"},
- {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"},
- {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"},
- {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"},
+ {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"},
+ {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"},
+ {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"},
+ {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"},
+ {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"},
+ {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"},
+ {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"},
+ {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"},
+ {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"},
+ {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"},
+ {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"},
+ {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"},
+ {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"},
+ {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"},
+ {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"},
+ {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"},
+ {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"},
+ {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"},
+ {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"},
+ {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"},
+ {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"},
+ {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"},
+ {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"},
+ {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"},
+ {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"},
+ {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"},
+ {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"},
+ {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"},
+ {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"},
+ {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"},
+ {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"},
+ {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"},
]
[package.dependencies]
mypy-extensions = ">=1.0.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
-typing-extensions = ">=4.1.0"
+typing-extensions = ">=4.6.0"
[package.extras]
dmypy = ["psutil (>=4.0)"]
+faster-cache = ["orjson"]
install-types = ["pip"]
mypyc = ["setuptools (>=50)"]
reports = ["lxml"]
@@ -788,24 +849,24 @@ files = [
[[package]]
name = "packaging"
-version = "23.2"
+version = "24.2"
description = "Core utilities for Python packages"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
- {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
+ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
+ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
[[package]]
name = "pluggy"
-version = "1.3.0"
+version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
- {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
+ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+ {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras]
@@ -813,84 +874,31 @@ dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
-name = "pycurl"
-version = "7.45.3"
-description = "PycURL -- A Python Interface To The cURL library"
+name = "pycparser"
+version = "2.22"
+description = "C parser in Python"
optional = true
-python-versions = ">=3.5"
+python-versions = ">=3.8"
files = [
- {file = "pycurl-7.45.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86f66d334deaaab20a576fb785587566081407adc703318203fe26e43277ef12"},
- {file = "pycurl-7.45.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:205983e87d6aa0b6e93ec7320060de44efaa905ecc5d13f70cbe38c65684c5c4"},
- {file = "pycurl-7.45.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbd4a6b8654b779089c5a44af1c65c1419c2cd60718780df6d8f354eb35d6d55"},
- {file = "pycurl-7.45.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5ebc6a0ac60c371a9efaf7d55dec5820f76fdafb43a3be1e390011339dc329ae"},
- {file = "pycurl-7.45.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:2facab1c35600088cb82b5b093bd700bfbd1e3191deab24f7d1803d9dc5b76fc"},
- {file = "pycurl-7.45.3-cp310-cp310-win32.whl", hash = "sha256:7cfca02d70579853041063e53ca713d31161b8831b98d4f68c3554dc0448beec"},
- {file = "pycurl-7.45.3-cp310-cp310-win_amd64.whl", hash = "sha256:8451e8475051f16eb4776380384699cb8ddd10ea8410bcbfaee5a6fc4c046de6"},
- {file = "pycurl-7.45.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1610cc45b5bc8b39bc18b981d0473e59ef41226ee467eaa8fbfc7276603ef5af"},
- {file = "pycurl-7.45.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c854885398410fa6e88fc29f7a420a3c13b88bae9b4e10a804437b582e24f58b"},
- {file = "pycurl-7.45.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:921c9db0c3128481954f625b3b1bc10c730100aa944d54643528f716676439ee"},
- {file = "pycurl-7.45.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:483f3aa5d1bc8cff5657ad96f68e1d89281f971a7b6aa93408a31e3199981ea9"},
- {file = "pycurl-7.45.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1e0d32d6ed3a7ba13dbbd3a6fb50ca76c40c70e6bc6fe347f90677478d3422c7"},
- {file = "pycurl-7.45.3-cp311-cp311-win32.whl", hash = "sha256:beaaa4450e23d41dd0c2f2f47a4f8a171210271543550c2c556090c7eeea88f5"},
- {file = "pycurl-7.45.3-cp311-cp311-win_amd64.whl", hash = "sha256:dd33fd9de8907a6275c70113124aeb7eea672c1324f5d5423f203738b341697d"},
- {file = "pycurl-7.45.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0c41a172d5e8a5cdd8328cc8134f47b2a57960ac677f7cda8520eaa9fbe7d990"},
- {file = "pycurl-7.45.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13006b62c157bb4483c58e1abdced6df723c9399255a4f5f6bb7f8e425106679"},
- {file = "pycurl-7.45.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27f4c5c20c86a9a823677316724306fb1ce3b25ec568efd52026dc6c563e5b29"},
- {file = "pycurl-7.45.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c2c246bc29e8762ff4c8a833ac5b4da4c797d16ab138286e8aec9b0c0a0da2d4"},
- {file = "pycurl-7.45.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3d07c5daef2d0d85949e32ec254ee44232bb57febb0634194379dd14d1ff4f87"},
- {file = "pycurl-7.45.3-cp312-cp312-win32.whl", hash = "sha256:9f7afe5ef0e4750ac4515baebc251ee94aaefe5de6e2e8a24668473128d69904"},
- {file = "pycurl-7.45.3-cp312-cp312-win_amd64.whl", hash = "sha256:3648ed9a57a6b704673faeab3dc64d1469cc69f2bc1ed8227ffa0f84e147c500"},
- {file = "pycurl-7.45.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c0915ea139f66a289edc4f9de10cb45078af1bb950491c5612969864236a2e7e"},
- {file = "pycurl-7.45.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:43c5e61a58783ddf78ef84949f6bb6e52e092a13ec67678e9a9e21071ecf5b80"},
- {file = "pycurl-7.45.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bf613844a1647fe3d2bba1f5c9c96a62a85280123a57a8a0c8d2f37d518bc10a"},
- {file = "pycurl-7.45.3-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:936afd9c5ff7fe7457065e878a279811787778f472f9a4e8c5df79e7728358e2"},
- {file = "pycurl-7.45.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:dbf816a6d0cb71e7fd06609246bbea4eaf100649d9decf49e4eb329594f70be7"},
- {file = "pycurl-7.45.3-cp38-cp38-win32.whl", hash = "sha256:2c8a2ce568193f9f84763717d8961cec0db4ec1aa08c6bcf4d90da5eb72bec86"},
- {file = "pycurl-7.45.3-cp38-cp38-win_amd64.whl", hash = "sha256:80ac7c17e69ca6b76ccccb4255f7c29a2a36e5b69eb10c2adba82135d43afe8c"},
- {file = "pycurl-7.45.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fa7751b614d9aa82d7a0f49ca90924c29c6cedf85a2f8687fb6a772dbfe48711"},
- {file = "pycurl-7.45.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b129e9ee07f80b4af957607917af46ab517b0c4e746692f6d9e50e973edba8d8"},
- {file = "pycurl-7.45.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a0f920582b8713ca87d5a288a7532607bc4454275d733fc880650d602dbe3c67"},
- {file = "pycurl-7.45.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c7c13e4268550cde14a6f4743cc8bd8c035d4cd36514d58eff70276d68954b6f"},
- {file = "pycurl-7.45.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:0f0e1251a608ffd75fc502f4014442e554c67d3d7a1b0a839c35efb6ad2f8bf8"},
- {file = "pycurl-7.45.3-cp39-cp39-win32.whl", hash = "sha256:51a40a56c58e63dac6145829f9e9bd66e5867a9f0741bcb9ffefab619851d44f"},
- {file = "pycurl-7.45.3-cp39-cp39-win_amd64.whl", hash = "sha256:e08a06802c8c8a9d04cf3319f9230ec09062c55d2550bd48f8ada1df1431adcf"},
- {file = "pycurl-7.45.3.tar.gz", hash = "sha256:8c2471af9079ad798e1645ec0b0d3d4223db687379d17dd36a70637449f81d6b"},
+ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
+ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
[[package]]
name = "pydantic"
-version = "2.7.4"
+version = "2.10.0"
description = "Data validation using Python type hints"
optional = true
python-versions = ">=3.8"
files = [
- {file = "pydantic-2.7.4-py3-none-any.whl", hash = "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"},
- {file = "pydantic-2.7.4.tar.gz", hash = "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52"},
-]
-
-[package.dependencies]
-annotated-types = ">=0.4.0"
-pydantic-core = "2.18.4"
-typing-extensions = ">=4.6.1"
-
-[package.extras]
-email = ["email-validator (>=2.0.0)"]
-
-[[package]]
-name = "pydantic"
-version = "2.9.2"
-description = "Data validation using Python type hints"
-optional = true
-python-versions = ">=3.8"
-files = [
- {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"},
- {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"},
+ {file = "pydantic-2.10.0-py3-none-any.whl", hash = "sha256:5e7807ba9201bdf61b1b58aa6eb690916c40a47acfb114b1b4fef3e7fd5b30fc"},
+ {file = "pydantic-2.10.0.tar.gz", hash = "sha256:0aca0f045ff6e2f097f1fe89521115335f15049eeb8a7bef3dafe4b19a74e289"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
-pydantic-core = "2.23.4"
-typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""}
+pydantic-core = "2.27.0"
+typing-extensions = ">=4.12.2"
[package.extras]
email = ["email-validator (>=2.0.0)"]
@@ -898,205 +906,142 @@ timezone = ["tzdata"]
[[package]]
name = "pydantic-core"
-version = "2.18.4"
+version = "2.27.0"
description = "Core functionality for Pydantic validation and serialization"
optional = true
python-versions = ">=3.8"
files = [
- {file = "pydantic_core-2.18.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4"},
- {file = "pydantic_core-2.18.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26"},
- {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a"},
- {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851"},
- {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d"},
- {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724"},
- {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be"},
- {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb"},
- {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c"},
- {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e"},
- {file = "pydantic_core-2.18.4-cp310-none-win32.whl", hash = "sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc"},
- {file = "pydantic_core-2.18.4-cp310-none-win_amd64.whl", hash = "sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0"},
- {file = "pydantic_core-2.18.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d"},
- {file = "pydantic_core-2.18.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4"},
- {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4"},
- {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2"},
- {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd"},
- {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d"},
- {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8"},
- {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951"},
- {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2"},
- {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9"},
- {file = "pydantic_core-2.18.4-cp311-none-win32.whl", hash = "sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558"},
- {file = "pydantic_core-2.18.4-cp311-none-win_amd64.whl", hash = "sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b"},
- {file = "pydantic_core-2.18.4-cp311-none-win_arm64.whl", hash = "sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805"},
- {file = "pydantic_core-2.18.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2"},
- {file = "pydantic_core-2.18.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef"},
- {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695"},
- {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34"},
- {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1"},
- {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077"},
- {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3"},
- {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9"},
- {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c"},
- {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8"},
- {file = "pydantic_core-2.18.4-cp312-none-win32.whl", hash = "sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07"},
- {file = "pydantic_core-2.18.4-cp312-none-win_amd64.whl", hash = "sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a"},
- {file = "pydantic_core-2.18.4-cp312-none-win_arm64.whl", hash = "sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f"},
- {file = "pydantic_core-2.18.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:823be1deb01793da05ecb0484d6c9e20baebb39bd42b5d72636ae9cf8350dbd2"},
- {file = "pydantic_core-2.18.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebef0dd9bf9b812bf75bda96743f2a6c5734a02092ae7f721c048d156d5fabae"},
- {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae1d6df168efb88d7d522664693607b80b4080be6750c913eefb77e34c12c71a"},
- {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9899c94762343f2cc2fc64c13e7cae4c3cc65cdfc87dd810a31654c9b7358cc"},
- {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99457f184ad90235cfe8461c4d70ab7dd2680e28821c29eca00252ba90308c78"},
- {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18f469a3d2a2fdafe99296a87e8a4c37748b5080a26b806a707f25a902c040a8"},
- {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cdf28938ac6b8b49ae5e92f2735056a7ba99c9b110a474473fd71185c1af5d"},
- {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:938cb21650855054dc54dfd9120a851c974f95450f00683399006aa6e8abb057"},
- {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:44cd83ab6a51da80fb5adbd9560e26018e2ac7826f9626bc06ca3dc074cd198b"},
- {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:972658f4a72d02b8abfa2581d92d59f59897d2e9f7e708fdabe922f9087773af"},
- {file = "pydantic_core-2.18.4-cp38-none-win32.whl", hash = "sha256:1d886dc848e60cb7666f771e406acae54ab279b9f1e4143babc9c2258213daa2"},
- {file = "pydantic_core-2.18.4-cp38-none-win_amd64.whl", hash = "sha256:bb4462bd43c2460774914b8525f79b00f8f407c945d50881568f294c1d9b4443"},
- {file = "pydantic_core-2.18.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528"},
- {file = "pydantic_core-2.18.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223"},
- {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f"},
- {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154"},
- {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30"},
- {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443"},
- {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94"},
- {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23"},
- {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b"},
- {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a"},
- {file = "pydantic_core-2.18.4-cp39-none-win32.whl", hash = "sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d"},
- {file = "pydantic_core-2.18.4-cp39-none-win_amd64.whl", hash = "sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e"},
- {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d"},
- {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab"},
- {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4"},
- {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc"},
- {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507"},
- {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef"},
- {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d"},
- {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5"},
- {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312"},
- {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5"},
- {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e"},
- {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8"},
- {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f"},
- {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce"},
- {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee"},
- {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9"},
- {file = "pydantic_core-2.18.4.tar.gz", hash = "sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2ac6b919f7fed71b17fe0b4603c092a4c9b5bae414817c9c81d3c22d1e1bcc"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e015833384ca3e1a0565a79f5d953b0629d9138021c27ad37c92a9fa1af7623c"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db72e40628967f6dc572020d04b5f800d71264e0531c6da35097e73bdf38b003"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df45c4073bed486ea2f18757057953afed8dd77add7276ff01bccb79982cf46c"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:836a4bfe0cc6d36dc9a9cc1a7b391265bf6ce9d1eb1eac62ac5139f5d8d9a6fa"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bf1340ae507f6da6360b24179c2083857c8ca7644aab65807023cf35404ea8d"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ab325fc86fbc077284c8d7f996d904d30e97904a87d6fb303dce6b3de7ebba9"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1da0c98a85a6c6ed702d5556db3b09c91f9b0b78de37b7593e2de8d03238807a"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7b0202ebf2268954090209a84f9897345719e46a57c5f2c9b7b250ca0a9d3e63"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:35380671c3c921fe8adf31ad349dc6f7588b7e928dbe44e1093789734f607399"},
+ {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b4c19525c3538fbc0bbda6229f9682fb8199ce9ac37395880e6952798e00373"},
+ {file = "pydantic_core-2.27.0-cp310-none-win32.whl", hash = "sha256:333c840a1303d1474f491e7be0b718226c730a39ead0f7dab2c7e6a2f3855555"},
+ {file = "pydantic_core-2.27.0-cp310-none-win_amd64.whl", hash = "sha256:99b2863c1365f43f74199c980a3d40f18a218fbe683dd64e470199db426c4d6a"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4523c4009c3f39d948e01962223c9f5538602e7087a628479b723c939fab262d"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84af1cf7bfdcbc6fcf5a5f70cc9896205e0350306e4dd73d54b6a18894f79386"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e65466b31be1070b4a5b7dbfbd14b247884cb8e8b79c64fb0f36b472912dbaea"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a5c022bb0d453192426221605efc865373dde43b17822a264671c53b068ac20c"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bb69bf3b6500f195c3deb69c1205ba8fc3cb21d1915f1f158a10d6b1ef29b6a"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aa4d1b2eba9a325897308b3124014a142cdccb9f3e016f31d3ebee6b5ea5e75"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e96ca781e0c01e32115912ebdf7b3fb0780ce748b80d7d28a0802fa9fbaf44e"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b872c86d8d71827235c7077461c502feb2db3f87d9d6d5a9daa64287d75e4fa0"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:82e1ad4ca170e8af4c928b67cff731b6296e6a0a0981b97b2eb7c275cc4e15bd"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:eb40f828bc2f73f777d1eb8fee2e86cd9692a4518b63b6b5aa8af915dfd3207b"},
+ {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9a8fbf506fde1529a1e3698198fe64bfbe2e0c09557bc6a7dcf872e7c01fec40"},
+ {file = "pydantic_core-2.27.0-cp311-none-win32.whl", hash = "sha256:24f984fc7762ed5f806d9e8c4c77ea69fdb2afd987b4fd319ef06c87595a8c55"},
+ {file = "pydantic_core-2.27.0-cp311-none-win_amd64.whl", hash = "sha256:68950bc08f9735306322bfc16a18391fcaac99ded2509e1cc41d03ccb6013cfe"},
+ {file = "pydantic_core-2.27.0-cp311-none-win_arm64.whl", hash = "sha256:3eb8849445c26b41c5a474061032c53e14fe92a11a5db969f722a2716cd12206"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8117839a9bdbba86e7f9df57018fe3b96cec934c3940b591b0fd3fbfb485864a"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a291d0b4243a259c8ea7e2b84eb9ccb76370e569298875a7c5e3e71baf49057a"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e35afd9e10b2698e6f2f32256678cb23ca6c1568d02628033a837638b3ed12"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58ab0d979c969983cdb97374698d847a4acffb217d543e172838864636ef10d9"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d06b667e53320332be2bf6f9461f4a9b78092a079b8ce8634c9afaa7e10cd9f"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78f841523729e43e3928a364ec46e2e3f80e6625a4f62aca5c345f3f626c6e8a"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:400bf470e4327e920883b51e255617dfe4496d4e80c3fea0b5a5d0bf2c404dd4"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:951e71da6c89d354572098bada5ba5b5dc3a9390c933af8a614e37755d3d1840"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a51ce96224eadd1845150b204389623c8e129fde5a67a84b972bd83a85c6c40"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:483c2213a609e7db2c592bbc015da58b6c75af7360ca3c981f178110d9787bcf"},
+ {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:359e7951f04ad35111b5ddce184db3391442345d0ab073aa63a95eb8af25a5ef"},
+ {file = "pydantic_core-2.27.0-cp312-none-win32.whl", hash = "sha256:ee7d9d5537daf6d5c74a83b38a638cc001b648096c1cae8ef695b0c919d9d379"},
+ {file = "pydantic_core-2.27.0-cp312-none-win_amd64.whl", hash = "sha256:2be0ad541bb9f059954ccf8877a49ed73877f862529575ff3d54bf4223e4dd61"},
+ {file = "pydantic_core-2.27.0-cp312-none-win_arm64.whl", hash = "sha256:6e19401742ed7b69e51d8e4df3c03ad5ec65a83b36244479fd70edde2828a5d9"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5f2b19b8d6fca432cb3acf48cf5243a7bf512988029b6e6fd27e9e8c0a204d85"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c86679f443e7085ea55a7376462553996c688395d18ef3f0d3dbad7838f857a2"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:510b11e9c3b1a852876d1ccd8d5903684336d635214148637ceb27366c75a467"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb704155e73b833801c247f39d562229c0303f54770ca14fb1c053acb376cf10"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ce048deb1e033e7a865ca384770bccc11d44179cf09e5193a535c4c2f497bdc"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58560828ee0951bb125c6f2862fbc37f039996d19ceb6d8ff1905abf7da0bf3d"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb4785894936d7682635726613c44578c420a096729f1978cd061a7e72d5275"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2883b260f7a93235488699d39cbbd94fa7b175d3a8063fbfddd3e81ad9988cb2"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c6fcb3fa3855d583aa57b94cf146f7781d5d5bc06cb95cb3afece33d31aac39b"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e851a051f7260e6d688267eb039c81f05f23a19431bd7dfa4bf5e3cb34c108cd"},
+ {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edb1bfd45227dec8d50bc7c7d86463cd8728bcc574f9b07de7369880de4626a3"},
+ {file = "pydantic_core-2.27.0-cp313-none-win32.whl", hash = "sha256:678f66462058dd978702db17eb6a3633d634f7aa0deaea61e0a674152766d3fc"},
+ {file = "pydantic_core-2.27.0-cp313-none-win_amd64.whl", hash = "sha256:d28ca7066d6cdd347a50d8b725dc10d9a1d6a1cce09836cf071ea6a2d4908be0"},
+ {file = "pydantic_core-2.27.0-cp313-none-win_arm64.whl", hash = "sha256:6f4a53af9e81d757756508b57cae1cf28293f0f31b9fa2bfcb416cc7fb230f9d"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:e9f9feee7f334b72ceae46313333d002b56f325b5f04271b4ae2aadd9e993ae4"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:225bfff5d425c34e1fd562cef52d673579d59b967d9de06178850c4802af9039"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921ad596ff1a82f9c692b0758c944355abc9f0de97a4c13ca60ffc6d8dc15d4"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6354e18a9be37bfa124d6b288a87fb30c673745806c92956f1a25e3ae6e76b96"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ee4c2a75af9fe21269a4a0898c5425afb01af1f5d276063f57e2ae1bc64e191"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c91e3c04f5191fd3fb68764bddeaf02025492d5d9f23343b283870f6ace69708"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6ebfac28fd51890a61df36ef202adbd77d00ee5aca4a3dadb3d9ed49cfb929"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36aa167f69d8807ba7e341d67ea93e50fcaaf6bc433bb04939430fa3dab06f31"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e8d89c276234579cd3d095d5fa2a44eb10db9a218664a17b56363cddf226ff3"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:5cc822ab90a70ea3a91e6aed3afac570b276b1278c6909b1d384f745bd09c714"},
+ {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e15315691fe2253eb447503153acef4d7223dfe7e7702f9ed66539fcd0c43801"},
+ {file = "pydantic_core-2.27.0-cp38-none-win32.whl", hash = "sha256:dfa5f5c0a4c8fced1422dc2ca7eefd872d5d13eb33cf324361dbf1dbfba0a9fe"},
+ {file = "pydantic_core-2.27.0-cp38-none-win_amd64.whl", hash = "sha256:513cb14c0cc31a4dfd849a4674b20c46d87b364f997bbcb02282306f5e187abf"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:4148dc9184ab79e356dc00a4199dc0ee8647973332cb385fc29a7cced49b9f9c"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5fc72fbfebbf42c0856a824b8b0dc2b5cd2e4a896050281a21cfa6fed8879cb1"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:185ef205256cd8b38431205698531026979db89a79587725c1e55c59101d64e9"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:395e3e1148fa7809016231f8065f30bb0dc285a97b4dc4360cd86e17bab58af7"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33d14369739c5d07e2e7102cdb0081a1fa46ed03215e07f097b34e020b83b1ae"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7820bb0d65e3ce1e3e70b6708c2f66143f55912fa02f4b618d0f08b61575f12"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43b61989068de9ce62296cde02beffabcadb65672207fc51e7af76dca75e6636"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15e350efb67b855cd014c218716feea4986a149ed1f42a539edd271ee074a196"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:433689845288f9a1ee5714444e65957be26d30915f7745091ede4a83cfb2d7bb"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:3fd8bc2690e7c39eecdf9071b6a889ce7b22b72073863940edc2a0a23750ca90"},
+ {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:884f1806609c2c66564082540cffc96868c5571c7c3cf3a783f63f2fb49bd3cd"},
+ {file = "pydantic_core-2.27.0-cp39-none-win32.whl", hash = "sha256:bf37b72834e7239cf84d4a0b2c050e7f9e48bced97bad9bdf98d26b8eb72e846"},
+ {file = "pydantic_core-2.27.0-cp39-none-win_amd64.whl", hash = "sha256:31a2cae5f059329f9cfe3d8d266d3da1543b60b60130d186d9b6a3c20a346361"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4fb49cfdb53af5041aba909be00cccfb2c0d0a2e09281bf542371c5fd36ad04c"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:49633583eb7dc5cba61aaf7cdb2e9e662323ad394e543ee77af265736bcd3eaa"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:153017e3d6cd3ce979de06d84343ca424bb6092727375eba1968c8b4693c6ecb"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff63a92f6e249514ef35bc795de10745be0226eaea06eb48b4bbeaa0c8850a4a"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5982048129f40b082c2654de10c0f37c67a14f5ff9d37cf35be028ae982f26df"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:91bc66f878557313c2a6bcf396e7befcffe5ab4354cfe4427318968af31143c3"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:68ef5377eb582fa4343c9d0b57a5b094046d447b4c73dd9fbd9ffb216f829e7d"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c5726eec789ee38f2c53b10b1821457b82274f81f4f746bb1e666d8741fcfadb"},
+ {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c0c431e4be5c1a0c6654e0c31c661cd89e0ca956ef65305c3c3fd96f4e72ca39"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8e21d927469d04b39386255bf00d0feedead16f6253dcc85e9e10ddebc334084"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b51f964fcbb02949fc546022e56cdb16cda457af485e9a3e8b78ac2ecf5d77e"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a7fd4de38f7ff99a37e18fa0098c3140286451bc823d1746ba80cec5b433a1"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fda87808429c520a002a85d6e7cdadbf58231d60e96260976c5b8f9a12a8e13"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a150392102c402c538190730fda06f3bce654fc498865579a9f2c1d2b425833"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c9ed88b398ba7e3bad7bd64d66cc01dcde9cfcb7ec629a6fd78a82fa0b559d78"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:9fe94d9d2a2b4edd7a4b22adcd45814b1b59b03feb00e56deb2e89747aec7bfe"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d8b5ee4ae9170e2775d495b81f414cc20268041c42571530513496ba61e94ba3"},
+ {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d29e235ce13c91902ef3efc3d883a677655b3908b1cbc73dee816e5e1f8f7739"},
+ {file = "pydantic_core-2.27.0.tar.gz", hash = "sha256:f57783fbaf648205ac50ae7d646f27582fc706be3977e87c3c124e7a92407b10"},
]
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
-name = "pydantic-core"
-version = "2.23.4"
-description = "Core functionality for Pydantic validation and serialization"
+name = "pyjwt"
+version = "2.10.0"
+description = "JSON Web Token implementation in Python"
optional = true
-python-versions = ">=3.8"
+python-versions = ">=3.9"
files = [
- {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"},
- {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"},
- {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"},
- {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"},
- {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"},
- {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"},
- {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"},
- {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"},
- {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"},
- {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"},
- {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"},
- {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"},
- {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"},
- {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"},
- {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"},
- {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"},
- {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"},
- {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"},
- {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"},
- {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"},
- {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"},
- {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"},
- {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"},
- {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"},
- {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"},
- {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"},
- {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"},
- {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"},
- {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"},
- {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"},
- {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"},
- {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"},
- {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"},
- {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"},
- {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"},
- {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"},
- {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"},
- {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"},
- {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"},
- {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"},
- {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"},
- {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"},
- {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"},
- {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"},
- {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"},
- {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"},
- {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"},
- {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"},
- {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"},
- {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"},
- {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"},
- {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"},
- {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"},
- {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"},
- {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"},
- {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"},
- {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"},
- {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"},
- {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"},
- {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"},
- {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"},
- {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"},
- {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"},
- {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"},
- {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"},
- {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"},
- {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"},
- {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"},
- {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"},
- {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"},
- {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"},
- {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"},
- {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"},
- {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"},
- {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"},
+ {file = "PyJWT-2.10.0-py3-none-any.whl", hash = "sha256:543b77207db656de204372350926bed5a86201c4cbff159f623f79c7bb487a15"},
+ {file = "pyjwt-2.10.0.tar.gz", hash = "sha256:7628a7eb7938959ac1b26e819a1df0fd3259505627b575e4bad6d08f76db695c"},
]
-[package.dependencies]
-typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+[package.extras]
+crypto = ["cryptography (>=3.4.0)"]
+dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
+docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
+tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]]
name = "pytest"
-version = "7.4.3"
+version = "7.4.4"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.7"
files = [
- {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"},
- {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"},
+ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
+ {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
]
[package.dependencies]
@@ -1112,13 +1057,13 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
[[package]]
name = "pytest-asyncio"
-version = "0.21.1"
+version = "0.21.2"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.7"
files = [
- {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"},
- {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"},
+ {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"},
+ {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"},
]
[package.dependencies]
@@ -1130,17 +1075,17 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy
[[package]]
name = "pytest-mock"
-version = "3.12.0"
+version = "3.14.0"
description = "Thin-wrapper around the mock package for easier use with pytest"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"},
- {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"},
+ {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
+ {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
]
[package.dependencies]
-pytest = ">=5.0"
+pytest = ">=6.2.5"
[package.extras]
dev = ["pre-commit", "pytest-asyncio", "tox"]
@@ -1161,31 +1106,6 @@ docopt = ">=0.4.0"
pytest = ">=2.6.4"
watchdog = ">=0.6.0"
-[[package]]
-name = "python-dateutil"
-version = "2.9.0.post0"
-description = "Extensions to the standard Python datetime module"
-optional = true
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
-files = [
- {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
- {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
-]
-
-[package.dependencies]
-six = ">=1.5"
-
-[[package]]
-name = "pytz"
-version = "2024.2"
-description = "World timezone definitions, modern and historical"
-optional = true
-python-versions = "*"
-files = [
- {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"},
- {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"},
-]
-
[[package]]
name = "pyyaml"
version = "6.0.2"
@@ -1285,40 +1205,29 @@ requests = ">=2.0.1,<3.0.0"
[[package]]
name = "ruff"
-version = "0.6.2"
+version = "0.6.9"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
- {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"},
- {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"},
- {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"},
- {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"},
- {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"},
- {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"},
- {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"},
- {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"},
- {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"},
- {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"},
- {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"},
-]
-
-[[package]]
-name = "six"
-version = "1.16.0"
-description = "Python 2 and 3 compatibility utilities"
-optional = true
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
-files = [
- {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
- {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+ {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"},
+ {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"},
+ {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"},
+ {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"},
+ {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"},
+ {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"},
+ {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"},
+ {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"},
+ {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"},
+ {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"},
+ {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"},
+ {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"},
+ {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"},
+ {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"},
+ {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"},
+ {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"},
+ {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"},
+ {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"},
]
[[package]]
@@ -1364,7 +1273,6 @@ files = [
[package.dependencies]
anyio = ">=3.4.0,<5"
-typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
[package.extras]
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
@@ -1388,13 +1296,13 @@ typing = ["mypy (>=1.4)", "rich", "twisted"]
[[package]]
name = "tenacity"
-version = "9.0.0"
+version = "8.5.0"
description = "Retry code until it succeeds"
optional = true
python-versions = ">=3.8"
files = [
- {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"},
- {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"},
+ {file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"},
+ {file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"},
]
[package.extras]
@@ -1403,44 +1311,24 @@ test = ["pytest", "tornado (>=4.5)", "typeguard"]
[[package]]
name = "tomli"
-version = "2.0.1"
+version = "2.1.0"
description = "A lil' TOML parser"
optional = false
-python-versions = ">=3.7"
-files = [
- {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
- {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
-]
-
-[[package]]
-name = "tornado"
-version = "6.4.1"
-description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
-optional = true
python-versions = ">=3.8"
files = [
- {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"},
- {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"},
- {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"},
- {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"},
- {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"},
- {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"},
- {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"},
- {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"},
- {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"},
- {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"},
- {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"},
+ {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"},
+ {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"},
]
[[package]]
name = "typing-extensions"
-version = "4.12.0"
+version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
- {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"},
- {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"},
+ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
+ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
@@ -1462,117 +1350,163 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "uvicorn"
-version = "0.32.0"
+version = "0.32.1"
description = "The lightning-fast ASGI server."
optional = true
python-versions = ">=3.8"
files = [
- {file = "uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82"},
- {file = "uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e"},
+ {file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"},
+ {file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"},
]
[package.dependencies]
click = ">=7.0"
h11 = ">=0.8"
-typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
[package.extras]
-standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
-
-[[package]]
-name = "uvloop"
-version = "0.21.0"
-description = "Fast implementation of asyncio event loop on top of libuv"
-optional = true
-python-versions = ">=3.8.0"
-files = [
- {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"},
- {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"},
- {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"},
- {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"},
- {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"},
- {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"},
- {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"},
- {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"},
- {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"},
- {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"},
- {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"},
- {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"},
- {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"},
- {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"},
- {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"},
- {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"},
- {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"},
- {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"},
- {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"},
- {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"},
- {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"},
- {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"},
- {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"},
- {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"},
- {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"},
- {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"},
- {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"},
- {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"},
- {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"},
- {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"},
- {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"},
- {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"},
- {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"},
- {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"},
- {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"},
- {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"},
- {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"},
-]
-
-[package.extras]
-dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"]
-docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
-test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"]
+standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
[[package]]
name = "watchdog"
-version = "3.0.0"
+version = "6.0.0"
description = "Filesystem events monitoring"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.9"
files = [
- {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"},
- {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"},
- {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"},
- {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"},
- {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"},
- {file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"},
- {file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"},
- {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"},
- {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"},
- {file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"},
- {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"},
- {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"},
- {file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"},
- {file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"},
- {file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"},
- {file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"},
- {file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"},
- {file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"},
- {file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"},
- {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"},
- {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"},
- {file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"},
- {file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"},
- {file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"},
- {file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"},
- {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"},
- {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"},
+ {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"},
+ {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"},
+ {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"},
+ {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"},
+ {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"},
+ {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"},
+ {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"},
+ {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"},
+ {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"},
+ {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"},
+ {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"},
+ {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"},
+ {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"},
+ {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"},
+ {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"},
+ {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"},
+ {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"},
+ {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"},
+ {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"},
+ {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"},
+ {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"},
+ {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"},
+ {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"},
]
[package.extras]
watchmedo = ["PyYAML (>=3.10)"]
+[[package]]
+name = "watchfiles"
+version = "0.24.0"
+description = "Simple, modern and high performance file watching and code reload in python."
+optional = true
+python-versions = ">=3.8"
+files = [
+ {file = "watchfiles-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0"},
+ {file = "watchfiles-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c"},
+ {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361"},
+ {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3"},
+ {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571"},
+ {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd"},
+ {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a"},
+ {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e"},
+ {file = "watchfiles-0.24.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c"},
+ {file = "watchfiles-0.24.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188"},
+ {file = "watchfiles-0.24.0-cp310-none-win32.whl", hash = "sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735"},
+ {file = "watchfiles-0.24.0-cp310-none-win_amd64.whl", hash = "sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04"},
+ {file = "watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428"},
+ {file = "watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c"},
+ {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43"},
+ {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327"},
+ {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5"},
+ {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61"},
+ {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15"},
+ {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823"},
+ {file = "watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab"},
+ {file = "watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec"},
+ {file = "watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d"},
+ {file = "watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c"},
+ {file = "watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633"},
+ {file = "watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a"},
+ {file = "watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370"},
+ {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6"},
+ {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b"},
+ {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e"},
+ {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea"},
+ {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f"},
+ {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234"},
+ {file = "watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef"},
+ {file = "watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968"},
+ {file = "watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444"},
+ {file = "watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896"},
+ {file = "watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418"},
+ {file = "watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48"},
+ {file = "watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90"},
+ {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94"},
+ {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e"},
+ {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827"},
+ {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df"},
+ {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab"},
+ {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f"},
+ {file = "watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b"},
+ {file = "watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18"},
+ {file = "watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07"},
+ {file = "watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366"},
+ {file = "watchfiles-0.24.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318"},
+ {file = "watchfiles-0.24.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05"},
+ {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c"},
+ {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83"},
+ {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c"},
+ {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b"},
+ {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b"},
+ {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91"},
+ {file = "watchfiles-0.24.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b"},
+ {file = "watchfiles-0.24.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22"},
+ {file = "watchfiles-0.24.0-cp38-none-win32.whl", hash = "sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1"},
+ {file = "watchfiles-0.24.0-cp38-none-win_amd64.whl", hash = "sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1"},
+ {file = "watchfiles-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886"},
+ {file = "watchfiles-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f"},
+ {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855"},
+ {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b"},
+ {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430"},
+ {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3"},
+ {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a"},
+ {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9"},
+ {file = "watchfiles-0.24.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca"},
+ {file = "watchfiles-0.24.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e"},
+ {file = "watchfiles-0.24.0-cp39-none-win32.whl", hash = "sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da"},
+ {file = "watchfiles-0.24.0-cp39-none-win_amd64.whl", hash = "sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f"},
+ {file = "watchfiles-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f"},
+ {file = "watchfiles-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b"},
+ {file = "watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4"},
+ {file = "watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a"},
+ {file = "watchfiles-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be"},
+ {file = "watchfiles-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5"},
+ {file = "watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777"},
+ {file = "watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e"},
+ {file = "watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1"},
+]
+
+[package.dependencies]
+anyio = ">=3.0.0"
+
[extras]
-inmem = ["langgraph-api-inmem"]
+inmem = ["langgraph-api"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0,<4.0"
-content-hash = "5a3dc8012db6a4cd103de557563e23f92b825caddd1d83c838282211a7aab4e3"
+content-hash = "624dc1a2a5c8a20ef781ed370e106f29da98e7c7235c797ff6a933a3ad20b500"
diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml
index bc64904ff..63f6f8d9a 100644
--- a/libs/cli/pyproject.toml
+++ b/libs/cli/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
-version = "0.1.56"
+version = "0.1.57"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
@@ -14,7 +14,7 @@ langgraph = "langgraph_cli.cli:cli"
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
click = "^8.1.7"
-langgraph-api-inmem = { version = ">=0.0.3,<0.1.0", optional = true }
+langgraph-api = { version = ">=0.0.2,<0.1.0", optional = true , python=">=3.11,<4.0" }
[tool.poetry.group.dev.dependencies]
ruff = "^0.6.2"
@@ -26,7 +26,7 @@ pytest-watch = "^4.2.0"
mypy = "^1.10.0"
[tool.poetry.extras]
-inmem = ["langgraph-api-inmem"]
+inmem = ["langgraph-api"]
[tool.pytest.ini_options]
# --strict-markers will raise errors on unknown marks.
From 72dac006f4c81ff03ffe051f26405c8f505c3cc6 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 21 Nov 2024 07:56:33 -0800
Subject: [PATCH 017/149] build(deps): bump cross-spawn from 7.0.3 to 7.0.6 in
/libs/cli/js-examples (#2456)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from
7.0.3 to 7.0.6.
Changelog
Sourced from cross-spawn's
changelog.
7.0.6
(2024-11-18)
Bug Fixes
- update cross-spawn version to 7.0.5 in package-lock.json (f700743)
7.0.5
(2024-11-07)
Bug Fixes
- fix escaping bug introduced by backtracking (640d391)
7.0.4
(2024-11-07)
Bug Fixes
Commits
77cd97f
chore(release): 7.0.6
6717de4
chore: upgrade standard-version
f700743
fix: update cross-spawn version to 7.0.5 in package-lock.json
9a7e3b2
chore: fix build status badge
0852683
chore(release): 7.0.5
640d391
fix: fix escaping bug introduced by backtracking
bff0c87
chore: remove codecov
a7c6abc
chore: replace travis with github workflows
9b9246e
chore(release): 7.0.4
5ff3a07
fix: disable regexp backtracking (#160)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
libs/cli/js-examples/yarn.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/libs/cli/js-examples/yarn.lock b/libs/cli/js-examples/yarn.lock
index 034535b0e..49021efe5 100644
--- a/libs/cli/js-examples/yarn.lock
+++ b/libs/cli/js-examples/yarn.lock
@@ -1299,9 +1299,9 @@ create-jest@^29.7.0:
prompts "^2.0.1"
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
- version "7.0.3"
- resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
- integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+ version "7.0.6"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
+ integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
From 9bd430142a25b041c7603fb57debc5e66f7cc671 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 21 Nov 2024 07:56:56 -0800
Subject: [PATCH 018/149] build(deps-dev): bump aiohttp from 3.10.6 to 3.10.11
(#2454)
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.6 to
3.10.11.
Release notes
Sourced from aiohttp's
releases.
3.10.11
Bug fixes
-
Authentication provided by a redirect now takes precedence over
provided auth when making requests with the client -- by
:user:PLPeeters.
Related issues and pull requests on GitHub:
#9436.
-
Fixed :py:meth:WebSocketResponse.close()
<aiohttp.web.WebSocketResponse.close> to discard non-close
messages within its timeout window after sending close -- by
:user:lenard-mosys.
Related issues and pull requests on GitHub:
#9506.
-
Fixed a deadlock that could occur while attempting to get a new
connection slot after a timeout -- by :user:bdraco.
The connector was not cancellation-safe.
Related issues and pull requests on GitHub:
#9670,
#9671.
-
Fixed the WebSocket flow control calculation undercounting with
multi-byte data -- by :user:bdraco.
Related issues and pull requests on GitHub:
#9686.
-
Fixed incorrect parsing of chunk extensions with the pure Python
parser -- by :user:bdraco.
Related issues and pull requests on GitHub:
#9851.
-
Fixed system routes polluting the middleware cache -- by
:user:bdraco.
Related issues and pull requests on GitHub:
... (truncated)
Changelog
Sourced from aiohttp's
changelog.
3.10.11 (2024-11-13)
Bug fixes
-
Authentication provided by a redirect now takes precedence over
provided auth when making requests with the client -- by
:user:PLPeeters.
Related issues and pull requests on GitHub:
:issue:9436.
-
Fixed :py:meth:WebSocketResponse.close()
<aiohttp.web.WebSocketResponse.close> to discard non-close
messages within its timeout window after sending close -- by
:user:lenard-mosys.
Related issues and pull requests on GitHub:
:issue:9506.
-
Fixed a deadlock that could occur while attempting to get a new
connection slot after a timeout -- by :user:bdraco.
The connector was not cancellation-safe.
Related issues and pull requests on GitHub:
:issue:9670, :issue:9671.
-
Fixed the WebSocket flow control calculation undercounting with
multi-byte data -- by :user:bdraco.
Related issues and pull requests on GitHub:
:issue:9686.
-
Fixed incorrect parsing of chunk extensions with the pure Python
parser -- by :user:bdraco.
Related issues and pull requests on GitHub:
:issue:9851.
-
Fixed system routes polluting the middleware cache -- by
:user:bdraco.
... (truncated)
Commits
3e09325
Remove 3.10.11rc0 from 3.10 changelog (#9858)
beb7b74
Release 3.10.11 (#9857)
259edc3
[PR #9851/541d86d
backport][3.10] Fix incorrect parsing of chunk extensions w...
bc15db6
[PR #9852/249855a
backport][3.10] Fix system routes polluting the middleware ...
158bf30
Release 3.10.11rc0 (#9848)
e5917cd
[PR #9844/fabf3884
backport][3.10] Fix compressed get request benchmark paylo...
68a1f42
[PR #9840/cc5fa316
backport][3.10] Add benchmark for sending compressed paylo...
4f4b90f
[PR #9835/32ccfc9a
backport][3.10] Adjust client payload benchmarks to better...
f3dd0f9
[PR #9832/006f4070
backport][3.10] Increase allowed import time for Python 3....
f2aab2e
[PR #9827/14fcfd4c
backport][3.10] Adjust client GET read benchmarks to inclu...
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
poetry.lock | 186 ++++++++++++++++++++++++++--------------------------
1 file changed, 93 insertions(+), 93 deletions(-)
diff --git a/poetry.lock b/poetry.lock
index 7d0162abb..caec1f3f3 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -13,108 +13,108 @@ files = [
[[package]]
name = "aiohttp"
-version = "3.10.6"
+version = "3.10.11"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.8"
files = [
- {file = "aiohttp-3.10.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:682836fc672972cc3101cc9e30d49c5f7e8f1d010478d46119fe725a4545acfd"},
- {file = "aiohttp-3.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:289fa8a20018d0d5aa9e4b35d899bd51bcb80f0d5f365d9a23e30dac3b79159b"},
- {file = "aiohttp-3.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8617c96a20dd57e7e9d398ff9d04f3d11c4d28b1767273a5b1a018ada5a654d3"},
- {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdbeff1b062751c2a2a55b171f7050fb7073633c699299d042e962aacdbe1a07"},
- {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ea35d849cdd4a9268f910bff4497baebbc1aa3f2f625fd8ccd9ac99c860c621"},
- {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473961b3252f3b949bb84873d6e268fb6d8aa0ccc6eb7404fa58c76a326bb8e1"},
- {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d2665c5df629eb2f981dab244c01bfa6cdc185f4ffa026639286c4d56fafb54"},
- {file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25d92f794f1332f656e3765841fc2b7ad5c26c3f3d01e8949eeb3495691cf9f4"},
- {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9bd6b2033993d5ae80883bb29b83fb2b432270bbe067c2f53cc73bb57c46065f"},
- {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d7f408c43f5e75ea1edc152fb375e8f46ef916f545fb66d4aebcbcfad05e2796"},
- {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:cf8b8560aa965f87bf9c13bf9fed7025993a155ca0ce8422da74bf46d18c2f5f"},
- {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14477c4e52e2f17437b99893fd220ffe7d7ee41df5ebf931a92b8ca82e6fd094"},
- {file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb138fbf9f53928e779650f5ed26d0ea1ed8b2cab67f0ea5d63afa09fdc07593"},
- {file = "aiohttp-3.10.6-cp310-cp310-win32.whl", hash = "sha256:9843d683b8756971797be171ead21511d2215a2d6e3c899c6e3107fbbe826791"},
- {file = "aiohttp-3.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:f8b8e49fe02f744d38352daca1dbef462c3874900bd8166516f6ea8e82b5aacf"},
- {file = "aiohttp-3.10.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52e54fd776ad0da1006708762213b079b154644db54bcfc62f06eaa5b896402"},
- {file = "aiohttp-3.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:995ab1a238fd0d19dc65f2d222e5eb064e409665c6426a3e51d5101c1979ee84"},
- {file = "aiohttp-3.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0749c4d5a08a802dd66ecdf59b2df4d76b900004017468a7bb736c3b5a3dd902"},
- {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e05b39158f2af0e2438cc2075cfc271f4ace0c3cc4a81ec95b27a0432e161951"},
- {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9f196c970db2dcde4f24317e06615363349dc357cf4d7a3b0716c20ac6d7bcd"},
- {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47647c8af04a70e07a2462931b0eba63146a13affa697afb4ecbab9d03a480ce"},
- {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c0efe7e99f6d94d63274c06344bd0e9c8daf184ce5602a29bc39e00a18720"},
- {file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9721cdd83a994225352ca84cd537760d41a9da3c0eacb3ff534747ab8fba6d0"},
- {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b82c8ebed66ce182893e7c0b6b60ba2ace45b1df104feb52380edae266a4850"},
- {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b169f8e755e541b72e714b89a831b315bbe70db44e33fead28516c9e13d5f931"},
- {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0be3115753baf8b4153e64f9aa7bf6c0c64af57979aa900c31f496301b374570"},
- {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e1f80cd17d81a404b6e70ef22bfe1870bafc511728397634ad5f5efc8698df56"},
- {file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6419728b08fb6380c66a470d2319cafcec554c81780e2114b7e150329b9a9a7f"},
- {file = "aiohttp-3.10.6-cp311-cp311-win32.whl", hash = "sha256:bd294dcdc1afdc510bb51d35444003f14e327572877d016d576ac3b9a5888a27"},
- {file = "aiohttp-3.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:bf861da9a43d282d6dd9dcd64c23a0fccf2c5aa5cd7c32024513c8c79fb69de3"},
- {file = "aiohttp-3.10.6-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:2708baccdc62f4b1251e59c2aac725936a900081f079b88843dabcab0feeeb27"},
- {file = "aiohttp-3.10.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7475da7a5e2ccf1a1c86c8fee241e277f4874c96564d06f726d8df8e77683ef7"},
- {file = "aiohttp-3.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:02108326574ff60267b7b35b17ac5c0bbd0008ccb942ce4c48b657bb90f0b8aa"},
- {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:029a019627b37fa9eac5c75cc54a6bb722c4ebbf5a54d8c8c0fb4dd8facf2702"},
- {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a637d387db6fdad95e293fab5433b775fd104ae6348d2388beaaa60d08b38c4"},
- {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1a16f3fc1944c61290d33c88dc3f09ba62d159b284c38c5331868425aca426"},
- {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81b292f37969f9cc54f4643f0be7dacabf3612b3b4a65413661cf6c350226787"},
- {file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0754690a3a26e819173a34093798c155bafb21c3c640bff13be1afa1e9d421f9"},
- {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:164ecd32e65467d86843dbb121a6666c3deb23b460e3f8aefdcaacae79eb718a"},
- {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:438c5863feb761f7ca3270d48c292c334814459f61cc12bab5ba5b702d7c9e56"},
- {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ba18573bb1de1063d222f41de64a0d3741223982dcea863b3f74646faf618ec7"},
- {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c82a94ddec996413a905f622f3da02c4359952aab8d817c01cf9915419525e95"},
- {file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92351aa5363fc3c1f872ca763f86730ced32b01607f0c9662b1fa711087968d0"},
- {file = "aiohttp-3.10.6-cp312-cp312-win32.whl", hash = "sha256:3e15e33bfc73fa97c228f72e05e8795e163a693fd5323549f49367c76a6e5883"},
- {file = "aiohttp-3.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:fe517113fe4d35d9072b826c3e147d63c5f808ca8167d450b4f96c520c8a1d8d"},
- {file = "aiohttp-3.10.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:482f74057ea13d387a7549d7a7ecb60e45146d15f3e58a2d93a0ad2d5a8457cd"},
- {file = "aiohttp-3.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:03fa40d1450ee5196e843315ddf74a51afc7e83d489dbfc380eecefea74158b1"},
- {file = "aiohttp-3.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e52e59ed5f4cc3a3acfe2a610f8891f216f486de54d95d6600a2c9ba1581f4d"},
- {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b3935a22c9e41a8000d90588bed96cf395ef572dbb409be44c6219c61d900d"},
- {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bef1480ee50f75abcfcb4b11c12de1005968ca9d0172aec4a5057ba9f2b644f"},
- {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:671745ea7db19693ce867359d503772177f0b20fa8f6ee1e74e00449f4c4151d"},
- {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b50b367308ca8c12e0b50cba5773bc9abe64c428d3fd2bbf5cd25aab37c77bf"},
- {file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a504d7cdb431a777d05a124fd0b21efb94498efa743103ea01b1e3136d2e4fb"},
- {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66bc81361131763660b969132a22edce2c4d184978ba39614e8f8f95db5c95f8"},
- {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:27cf19a38506e2e9f12fc17e55f118f04897b0a78537055d93a9de4bf3022e3d"},
- {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3468b39f977a11271517c6925b226720e148311039a380cc9117b1e2258a721f"},
- {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9d26da22a793dfd424be1050712a70c0afd96345245c29aced1e35dbace03413"},
- {file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:844d48ff9173d0b941abed8b2ea6a412f82b56d9ab1edb918c74000c15839362"},
- {file = "aiohttp-3.10.6-cp313-cp313-win32.whl", hash = "sha256:2dd56e3c43660ed3bea67fd4c5025f1ac1f9ecf6f0b991a6e5efe2e678c490c5"},
- {file = "aiohttp-3.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:c91781d969fbced1993537f45efe1213bd6fccb4b37bfae2a026e20d6fbed206"},
- {file = "aiohttp-3.10.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4407a80bca3e694f2d2a523058e20e1f9f98a416619e04f6dc09dc910352ac8b"},
- {file = "aiohttp-3.10.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1cb045ec5961f51af3e2c08cd6fe523f07cc6e345033adee711c49b7b91bb954"},
- {file = "aiohttp-3.10.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4fabdcdc781a36b8fd7b2ca9dea8172f29a99e11d00ca0f83ffeb50958da84a1"},
- {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a9f42efcc2681790595ab3d03c0e52d01edc23a0973ea09f0dc8d295e12b8e"},
- {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cca776a440795db437d82c07455761c85bbcf3956221c3c23b8c93176c278ce7"},
- {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5582de171f0898139cf51dd9fcdc79b848e28d9abd68e837f0803fc9f30807b1"},
- {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:370e2d47575c53c817ee42a18acc34aad8da4dbdaac0a6c836d58878955f1477"},
- {file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:444d1704e2af6b30766debed9be8a795958029e552fe77551355badb1944012c"},
- {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40271a2a375812967401c9ca8077de9368e09a43a964f4dce0ff603301ec9358"},
- {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f3af26f86863fad12e25395805bb0babbd49d512806af91ec9708a272b696248"},
- {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4752df44df48fd42b80f51d6a97553b482cda1274d9dc5df214a3a1aa5d8f018"},
- {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2cd5290ab66cfca2f90045db2cc6434c1f4f9fbf97c9f1c316e785033782e7d2"},
- {file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3427031064b0d5c95647e6369c4aa3c556402f324a3e18107cb09517abe5f962"},
- {file = "aiohttp-3.10.6-cp38-cp38-win32.whl", hash = "sha256:614fc21e86adc28e4165a6391f851a6da6e9cbd7bb232d0df7718b453a89ee98"},
- {file = "aiohttp-3.10.6-cp38-cp38-win_amd64.whl", hash = "sha256:58c5d7318a136a3874c78717dd6de57519bc64f6363c5827c2b1cb775bea71dd"},
- {file = "aiohttp-3.10.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5db26bbca8e7968c4c977a0c640e0b9ce7224e1f4dcafa57870dc6ee28e27de6"},
- {file = "aiohttp-3.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fb4216e3ec0dbc01db5ba802f02ed78ad8f07121be54eb9e918448cc3f61b7c"},
- {file = "aiohttp-3.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a976ef488f26e224079deb3d424f29144c6d5ba4ded313198169a8af8f47fb82"},
- {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a86610174de8a85a920e956e2d4f9945e7da89f29a00e95ac62a4a414c4ef4e"},
- {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:217791c6a399cc4f2e6577bb44344cba1f5714a2aebf6a0bea04cfa956658284"},
- {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba3662d41abe2eab0eeec7ee56f33ef4e0b34858f38abf24377687f9e1fb00a5"},
- {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4dfa5ad4bce9ca30a76117fbaa1c1decf41ebb6c18a4e098df44298941566f9"},
- {file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0009258e97502936d3bd5bf2ced15769629097d0abb81e6495fba1047824fe0"},
- {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0a75d5c9fb4f06c41d029ae70ad943c3a844c40c0a769d12be4b99b04f473d3d"},
- {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8198b7c002aae2b40b2d16bfe724b9a90bcbc9b78b2566fc96131ef4e382574d"},
- {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4611db8c907f90fe86be112efdc2398cd7b4c8eeded5a4f0314b70fdea8feab0"},
- {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ff99ae06eef85c7a565854826114ced72765832ee16c7e3e766c5e4c5b98d20e"},
- {file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7641920bdcc7cd2d3ddfb8bb9133a6c9536b09dbd49490b79e125180b2d25b93"},
- {file = "aiohttp-3.10.6-cp39-cp39-win32.whl", hash = "sha256:e2e7d5591ea868d5ec82b90bbeb366a198715672841d46281b623e23079593db"},
- {file = "aiohttp-3.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:b504c08c45623bf5c7ca41be380156d925f00199b3970efd758aef4a77645feb"},
- {file = "aiohttp-3.10.6.tar.gz", hash = "sha256:d2578ef941be0c2ba58f6f421a703527d08427237ed45ecb091fed6f83305336"},
+ {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5077b1a5f40ffa3ba1f40d537d3bec4383988ee51fbba6b74aa8fb1bc466599e"},
+ {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d6a14a4d93b5b3c2891fca94fa9d41b2322a68194422bef0dd5ec1e57d7d298"},
+ {file = "aiohttp-3.10.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffbfde2443696345e23a3c597049b1dd43049bb65337837574205e7368472177"},
+ {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20b3d9e416774d41813bc02fdc0663379c01817b0874b932b81c7f777f67b217"},
+ {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b943011b45ee6bf74b22245c6faab736363678e910504dd7531a58c76c9015a"},
+ {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48bc1d924490f0d0b3658fe5c4b081a4d56ebb58af80a6729d4bd13ea569797a"},
+ {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e12eb3f4b1f72aaaf6acd27d045753b18101524f72ae071ae1c91c1cd44ef115"},
+ {file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f14ebc419a568c2eff3c1ed35f634435c24ead2fe19c07426af41e7adb68713a"},
+ {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:72b191cdf35a518bfc7ca87d770d30941decc5aaf897ec8b484eb5cc8c7706f3"},
+ {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5ab2328a61fdc86424ee540d0aeb8b73bbcad7351fb7cf7a6546fc0bcffa0038"},
+ {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa93063d4af05c49276cf14e419550a3f45258b6b9d1f16403e777f1addf4519"},
+ {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:30283f9d0ce420363c24c5c2421e71a738a2155f10adbb1a11a4d4d6d2715cfc"},
+ {file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e5358addc8044ee49143c546d2182c15b4ac3a60be01c3209374ace05af5733d"},
+ {file = "aiohttp-3.10.11-cp310-cp310-win32.whl", hash = "sha256:e1ffa713d3ea7cdcd4aea9cddccab41edf6882fa9552940344c44e59652e1120"},
+ {file = "aiohttp-3.10.11-cp310-cp310-win_amd64.whl", hash = "sha256:778cbd01f18ff78b5dd23c77eb82987ee4ba23408cbed233009fd570dda7e674"},
+ {file = "aiohttp-3.10.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:80ff08556c7f59a7972b1e8919f62e9c069c33566a6d28586771711e0eea4f07"},
+ {file = "aiohttp-3.10.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c8f96e9ee19f04c4914e4e7a42a60861066d3e1abf05c726f38d9d0a466e695"},
+ {file = "aiohttp-3.10.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fb8601394d537da9221947b5d6e62b064c9a43e88a1ecd7414d21a1a6fba9c24"},
+ {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea224cf7bc2d8856d6971cea73b1d50c9c51d36971faf1abc169a0d5f85a382"},
+ {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db9503f79e12d5d80b3efd4d01312853565c05367493379df76d2674af881caa"},
+ {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f449a50cc33f0384f633894d8d3cd020e3ccef81879c6e6245c3c375c448625"},
+ {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82052be3e6d9e0c123499127782a01a2b224b8af8c62ab46b3f6197035ad94e9"},
+ {file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20063c7acf1eec550c8eb098deb5ed9e1bb0521613b03bb93644b810986027ac"},
+ {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:489cced07a4c11488f47aab1f00d0c572506883f877af100a38f1fedaa884c3a"},
+ {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea9b3bab329aeaa603ed3bf605f1e2a6f36496ad7e0e1aa42025f368ee2dc07b"},
+ {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ca117819d8ad113413016cb29774b3f6d99ad23c220069789fc050267b786c16"},
+ {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2dfb612dcbe70fb7cdcf3499e8d483079b89749c857a8f6e80263b021745c730"},
+ {file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9b615d3da0d60e7d53c62e22b4fd1c70f4ae5993a44687b011ea3a2e49051b8"},
+ {file = "aiohttp-3.10.11-cp311-cp311-win32.whl", hash = "sha256:29103f9099b6068bbdf44d6a3d090e0a0b2be6d3c9f16a070dd9d0d910ec08f9"},
+ {file = "aiohttp-3.10.11-cp311-cp311-win_amd64.whl", hash = "sha256:236b28ceb79532da85d59aa9b9bf873b364e27a0acb2ceaba475dc61cffb6f3f"},
+ {file = "aiohttp-3.10.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7480519f70e32bfb101d71fb9a1f330fbd291655a4c1c922232a48c458c52710"},
+ {file = "aiohttp-3.10.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f65267266c9aeb2287a6622ee2bb39490292552f9fbf851baabc04c9f84e048d"},
+ {file = "aiohttp-3.10.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7400a93d629a0608dc1d6c55f1e3d6e07f7375745aaa8bd7f085571e4d1cee97"},
+ {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f34b97e4b11b8d4eb2c3a4f975be626cc8af99ff479da7de49ac2c6d02d35725"},
+ {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e7b825da878464a252ccff2958838f9caa82f32a8dbc334eb9b34a026e2c636"},
+ {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9f92a344c50b9667827da308473005f34767b6a2a60d9acff56ae94f895f385"},
+ {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f1ab987a27b83c5268a17218463c2ec08dbb754195113867a27b166cd6087"},
+ {file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1dc0f4ca54842173d03322793ebcf2c8cc2d34ae91cc762478e295d8e361e03f"},
+ {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7ce6a51469bfaacff146e59e7fb61c9c23006495d11cc24c514a455032bcfa03"},
+ {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aad3cd91d484d065ede16f3cf15408254e2469e3f613b241a1db552c5eb7ab7d"},
+ {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f4df4b8ca97f658c880fb4b90b1d1ec528315d4030af1ec763247ebfd33d8b9a"},
+ {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e4e18a0a2d03531edbc06c366954e40a3f8d2a88d2b936bbe78a0c75a3aab3e"},
+ {file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ce66780fa1a20e45bc753cda2a149daa6dbf1561fc1289fa0c308391c7bc0a4"},
+ {file = "aiohttp-3.10.11-cp312-cp312-win32.whl", hash = "sha256:a919c8957695ea4c0e7a3e8d16494e3477b86f33067478f43106921c2fef15bb"},
+ {file = "aiohttp-3.10.11-cp312-cp312-win_amd64.whl", hash = "sha256:b5e29706e6389a2283a91611c91bf24f218962717c8f3b4e528ef529d112ee27"},
+ {file = "aiohttp-3.10.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:703938e22434d7d14ec22f9f310559331f455018389222eed132808cd8f44127"},
+ {file = "aiohttp-3.10.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9bc50b63648840854e00084c2b43035a62e033cb9b06d8c22b409d56eb098413"},
+ {file = "aiohttp-3.10.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f0463bf8b0754bc744e1feb61590706823795041e63edf30118a6f0bf577461"},
+ {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6c6dec398ac5a87cb3a407b068e1106b20ef001c344e34154616183fe684288"},
+ {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcaf2d79104d53d4dcf934f7ce76d3d155302d07dae24dff6c9fffd217568067"},
+ {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25fd5470922091b5a9aeeb7e75be609e16b4fba81cdeaf12981393fb240dd10e"},
+ {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbde2ca67230923a42161b1f408c3992ae6e0be782dca0c44cb3206bf330dee1"},
+ {file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:249c8ff8d26a8b41a0f12f9df804e7c685ca35a207e2410adbd3e924217b9006"},
+ {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:878ca6a931ee8c486a8f7b432b65431d095c522cbeb34892bee5be97b3481d0f"},
+ {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8663f7777ce775f0413324be0d96d9730959b2ca73d9b7e2c2c90539139cbdd6"},
+ {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd3f10b01f0c31481fba8d302b61603a2acb37b9d30e1d14e0f5a58b7b18a31"},
+ {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e8d8aad9402d3aa02fdc5ca2fe68bcb9fdfe1f77b40b10410a94c7f408b664d"},
+ {file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38e3c4f80196b4f6c3a85d134a534a56f52da9cb8d8e7af1b79a32eefee73a00"},
+ {file = "aiohttp-3.10.11-cp313-cp313-win32.whl", hash = "sha256:fc31820cfc3b2863c6e95e14fcf815dc7afe52480b4dc03393c4873bb5599f71"},
+ {file = "aiohttp-3.10.11-cp313-cp313-win_amd64.whl", hash = "sha256:4996ff1345704ffdd6d75fb06ed175938c133425af616142e7187f28dc75f14e"},
+ {file = "aiohttp-3.10.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:74baf1a7d948b3d640badeac333af581a367ab916b37e44cf90a0334157cdfd2"},
+ {file = "aiohttp-3.10.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:473aebc3b871646e1940c05268d451f2543a1d209f47035b594b9d4e91ce8339"},
+ {file = "aiohttp-3.10.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c2f746a6968c54ab2186574e15c3f14f3e7f67aef12b761e043b33b89c5b5f95"},
+ {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d110cabad8360ffa0dec8f6ec60e43286e9d251e77db4763a87dcfe55b4adb92"},
+ {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0099c7d5d7afff4202a0c670e5b723f7718810000b4abcbc96b064129e64bc7"},
+ {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0316e624b754dbbf8c872b62fe6dcb395ef20c70e59890dfa0de9eafccd2849d"},
+ {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a5f7ab8baf13314e6b2485965cbacb94afff1e93466ac4d06a47a81c50f9cca"},
+ {file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c891011e76041e6508cbfc469dd1a8ea09bc24e87e4c204e05f150c4c455a5fa"},
+ {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9208299251370ee815473270c52cd3f7069ee9ed348d941d574d1457d2c73e8b"},
+ {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:459f0f32c8356e8125f45eeff0ecf2b1cb6db1551304972702f34cd9e6c44658"},
+ {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:14cdc8c1810bbd4b4b9f142eeee23cda528ae4e57ea0923551a9af4820980e39"},
+ {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:971aa438a29701d4b34e4943e91b5e984c3ae6ccbf80dd9efaffb01bd0b243a9"},
+ {file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9a309c5de392dfe0f32ee57fa43ed8fc6ddf9985425e84bd51ed66bb16bce3a7"},
+ {file = "aiohttp-3.10.11-cp38-cp38-win32.whl", hash = "sha256:9ec1628180241d906a0840b38f162a3215114b14541f1a8711c368a8739a9be4"},
+ {file = "aiohttp-3.10.11-cp38-cp38-win_amd64.whl", hash = "sha256:9c6e0ffd52c929f985c7258f83185d17c76d4275ad22e90aa29f38e211aacbec"},
+ {file = "aiohttp-3.10.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cdc493a2e5d8dc79b2df5bec9558425bcd39aff59fc949810cbd0832e294b106"},
+ {file = "aiohttp-3.10.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3e70f24e7d0405be2348da9d5a7836936bf3a9b4fd210f8c37e8d48bc32eca6"},
+ {file = "aiohttp-3.10.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968b8fb2a5eee2770eda9c7b5581587ef9b96fbdf8dcabc6b446d35ccc69df01"},
+ {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deef4362af9493d1382ef86732ee2e4cbc0d7c005947bd54ad1a9a16dd59298e"},
+ {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:686b03196976e327412a1b094f4120778c7c4b9cff9bce8d2fdfeca386b89829"},
+ {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bf6d027d9d1d34e1c2e1645f18a6498c98d634f8e373395221121f1c258ace8"},
+ {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:099fd126bf960f96d34a760e747a629c27fb3634da5d05c7ef4d35ef4ea519fc"},
+ {file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c73c4d3dae0b4644bc21e3de546530531d6cdc88659cdeb6579cd627d3c206aa"},
+ {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0c5580f3c51eea91559db3facd45d72e7ec970b04528b4709b1f9c2555bd6d0b"},
+ {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fdf6429f0caabfd8a30c4e2eaecb547b3c340e4730ebfe25139779b9815ba138"},
+ {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d97187de3c276263db3564bb9d9fad9e15b51ea10a371ffa5947a5ba93ad6777"},
+ {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0acafb350cfb2eba70eb5d271f55e08bd4502ec35e964e18ad3e7d34d71f7261"},
+ {file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c13ed0c779911c7998a58e7848954bd4d63df3e3575f591e321b19a2aec8df9f"},
+ {file = "aiohttp-3.10.11-cp39-cp39-win32.whl", hash = "sha256:22b7c540c55909140f63ab4f54ec2c20d2635c0289cdd8006da46f3327f971b9"},
+ {file = "aiohttp-3.10.11-cp39-cp39-win_amd64.whl", hash = "sha256:7b26b1551e481012575dab8e3727b16fe7dd27eb2711d2e63ced7368756268fb"},
+ {file = "aiohttp-3.10.11.tar.gz", hash = "sha256:9dc2b8f3dcab2e39e0fa309c8da50c3b55e6f34ab25f1a71d3288f24924d33a7"},
]
[package.dependencies]
aiohappyeyeballs = ">=2.3.0"
aiosignal = ">=1.1.2"
-async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""}
+async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""}
attrs = ">=17.3.0"
frozenlist = ">=1.1.1"
multidict = ">=4.5,<7.0"
From f7788abbb65b8007e9423804787b274de58e03e9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 21 Nov 2024 07:58:30 -0800
Subject: [PATCH 019/149] build(deps-dev): bump starlette from 0.38.6 to 0.40.0
(#2421)
Bumps [starlette](https://github.com/encode/starlette) from 0.38.6 to
0.40.0.
Release notes
Sourced from starlette's
releases.
Version 0.40.0
This release fixes a Denial of service (DoS) via
multipart/form-data requests.
You can view the full security advisory:
GHSA-f96h-pmfr-66vw
Fixed
- Add
max_part_size to MultiPartParser to
limit the size of parts in multipart/form-data
requests fd038f3.
Version 0.39.2
Fixed
- Allow use of
request.url_for when only "app"
scope is available #2672.
- Fix internal type hints to support
python-multipart==0.0.12 #2708.
Full Changelog: https://github.com/encode/starlette/compare/0.39.1...0.39.2
Version 0.39.1
Fixed
- Avoid regex re-compilation in
responses.py and
schemas.py #2700.
- Improve performance of
get_route_path by removing
regular expression usage #2701.
- Consider
FileResponse.chunk_size when handling multiple
ranges #2703.
- Use
token_hex for generating multipart boundary strings
#2702.
Full Changelog: https://github.com/encode/starlette/compare/0.39.0...0.39.1
Version 0.39.0
Added
- Add support for HTTP Range to
FileResponse #2697
Full Changelog: https://github.com/encode/starlette/compare/0.38.6...0.39.0
Changelog
Sourced from starlette's
changelog.
0.40.0 (October 15, 2024)
This release fixes a Denial of service (DoS) via
multipart/form-data requests.
You can view the full security advisory:
GHSA-f96h-pmfr-66vw
Fixed
- Add
max_part_size to MultiPartParser to
limit the size of parts in multipart/form-data
requests fd038f3.
0.39.2 (September 29, 2024)
Fixed
- Allow use of
request.url_for when only "app"
scope is available #2672.
- Fix internal type hints to support
python-multipart==0.0.12 #2708.
0.39.1 (September 25, 2024)
Fixed
- Avoid regex re-compilation in
responses.py and
schemas.py #2700.
- Improve performance of
get_route_path by removing
regular expression usage
#2701.
- Consider
FileResponse.chunk_size when handling multiple
ranges #2703.
- Use
token_hex for generating multipart boundary strings
#2702.
0.39.0 (September 23, 2024)
Added
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
---
poetry.lock | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/poetry.lock b/poetry.lock
index caec1f3f3..d8f321637 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1292,18 +1292,18 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth
[[package]]
name = "fastapi"
-version = "0.115.0"
+version = "0.115.5"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
- {file = "fastapi-0.115.0-py3-none-any.whl", hash = "sha256:17ea427674467486e997206a5ab25760f6b09e069f099b96f5b55a32fb6f1631"},
- {file = "fastapi-0.115.0.tar.gz", hash = "sha256:f93b4ca3529a8ebc6fc3fcf710e5efa8de3df9b41570958abf1d97d843138004"},
+ {file = "fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796"},
+ {file = "fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289"},
]
[package.dependencies]
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
-starlette = ">=0.37.2,<0.39.0"
+starlette = ">=0.40.0,<0.42.0"
typing-extensions = ">=4.8.0"
[package.extras]
@@ -6446,13 +6446,13 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
[[package]]
name = "starlette"
-version = "0.38.6"
+version = "0.40.0"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.8"
files = [
- {file = "starlette-0.38.6-py3-none-any.whl", hash = "sha256:4517a1409e2e73ee4951214ba012052b9e16f60e90d73cfb06192c19203bbb05"},
- {file = "starlette-0.38.6.tar.gz", hash = "sha256:863a1588f5574e70a821dadefb41e4881ea451a47a3cd1b4df359d4ffefe5ead"},
+ {file = "starlette-0.40.0-py3-none-any.whl", hash = "sha256:c494a22fae73805376ea6bf88439783ecfba9aac88a43911b48c653437e784c4"},
+ {file = "starlette-0.40.0.tar.gz", hash = "sha256:1a3139688fb298ce5e2d661d37046a66ad996ce94be4d4983be019a23a04ea35"},
]
[package.dependencies]
From ceeb9636ee7c0fb1d3123401082e8d7a04bfa740 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 21 Nov 2024 08:00:26 -0800
Subject: [PATCH 020/149] build(deps-dev): bump notebook from 7.0.7 to 7.2.2 in
/libs/langgraph (#2411)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [notebook](https://github.com/jupyter/notebook) from 7.0.7 to
7.2.2.
Release notes
Sourced from notebook's
releases.
v7.2.2
7.2.2
(Full
Changelog)
Maintenance and upkeep improvements
Contributors to this release
(GitHub
contributors page for this release)
@github-actions
| @krassowski
| @RRosio
v7.2.1
7.2.1
(Full
Changelog)
Bugs fixed
Contributors to this release
(GitHub
contributors page for this release)
@github-actions
| @jtpio
| @meeseeksmachine
v7.2.0
7.2.0
(Full
Changelog)
Enhancements made
Bugs fixed
... (truncated)
Changelog
Sourced from notebook's
changelog.
7.2.2
(Full
Changelog)
Maintenance and upkeep improvements
Contributors to this release
(GitHub
contributors page for this release)
@github-actions
| @krassowski
| @RRosio
7.2.1
(Full
Changelog)
Bugs fixed
Contributors to this release
(GitHub
contributors page for this release)
@github-actions
| @jtpio
| @meeseeksmachine
7.2.0
(Full
Changelog)
Enhancements made
Bugs fixed
Maintenance and upkeep improvements
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
---
libs/langgraph/poetry.lock | 26 +++++++++++++-------------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/libs/langgraph/poetry.lock b/libs/langgraph/poetry.lock
index d337af2bc..5005e8149 100644
--- a/libs/langgraph/poetry.lock
+++ b/libs/langgraph/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "aiosqlite"
@@ -1253,13 +1253,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>
[[package]]
name = "jupyterlab"
-version = "4.3.0"
+version = "4.2.5"
description = "JupyterLab computational environment"
optional = false
python-versions = ">=3.8"
files = [
- {file = "jupyterlab-4.3.0-py3-none-any.whl", hash = "sha256:f67e1095ad61ae04349024f0b40345062ab108a0c6998d9810fec6a3c1a70cd5"},
- {file = "jupyterlab-4.3.0.tar.gz", hash = "sha256:7c6835cbf8df0af0ec8a39332e85ff11693fb9a468205343b4fc0bfbc74817e5"},
+ {file = "jupyterlab-4.2.5-py3-none-any.whl", hash = "sha256:73b6e0775d41a9fee7ee756c80f58a6bed4040869ccc21411dc559818874d321"},
+ {file = "jupyterlab-4.2.5.tar.gz", hash = "sha256:ae7f3a1b8cb88b4f55009ce79fa7c06f99d70cd63601ee4aa91815d054f46f75"},
]
[package.dependencies]
@@ -1280,9 +1280,9 @@ tornado = ">=6.2.0"
traitlets = "*"
[package.extras]
-dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.6.9)"]
-docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<8.1.0)", "sphinx-copybutton"]
-docs-screenshots = ["altair (==5.4.1)", "ipython (==8.16.1)", "ipywidgets (==8.1.5)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.2.post3)", "matplotlib (==3.9.2)", "nbconvert (>=7.0.0)", "pandas (==2.2.3)", "scipy (==1.14.1)", "vega-datasets (==0.9.0)"]
+dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.3.5)"]
+docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"]
+docs-screenshots = ["altair (==5.3.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.2)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.1.post2)", "matplotlib (==3.8.3)", "nbconvert (>=7.0.0)", "pandas (==2.2.1)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"]
test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"]
upgrade-extension = ["copier (>=9,<10)", "jinja2-time (<0.3)", "pydantic (<3.0)", "pyyaml-include (<3.0)", "tomli-w (<2.0)"]
@@ -1792,26 +1792,26 @@ files = [
[[package]]
name = "notebook"
-version = "7.0.7"
+version = "7.2.2"
description = "Jupyter Notebook - A web-based notebook environment for interactive computing"
optional = false
python-versions = ">=3.8"
files = [
- {file = "notebook-7.0.7-py3-none-any.whl", hash = "sha256:289b606d7e173f75a18beb1406ef411b43f97f7a9c55ba03efa3622905a62346"},
- {file = "notebook-7.0.7.tar.gz", hash = "sha256:3bcff00c17b3ac142ef5f436d50637d936b274cfa0b41f6ac0175363de9b4e09"},
+ {file = "notebook-7.2.2-py3-none-any.whl", hash = "sha256:c89264081f671bc02eec0ed470a627ed791b9156cad9285226b31611d3e9fe1c"},
+ {file = "notebook-7.2.2.tar.gz", hash = "sha256:2ef07d4220421623ad3fe88118d687bc0450055570cdd160814a59cf3a1c516e"},
]
[package.dependencies]
jupyter-server = ">=2.4.0,<3"
-jupyterlab = ">=4.0.2,<5"
-jupyterlab-server = ">=2.22.1,<3"
+jupyterlab = ">=4.2.0,<4.3"
+jupyterlab-server = ">=2.27.1,<3"
notebook-shim = ">=0.2,<0.3"
tornado = ">=6.2.0"
[package.extras]
dev = ["hatch", "pre-commit"]
docs = ["myst-parser", "nbsphinx", "pydata-sphinx-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"]
-test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.22.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"]
+test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.27.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"]
[[package]]
name = "notebook-shim"
From 7082e2613e107b0d9ed7749e7bafd4cbecd84698 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Thu, 21 Nov 2024 08:28:53 -0800
Subject: [PATCH 021/149] [CLI] Dotenv support (#2501)
---
libs/cli/langgraph_cli/cli.py | 1 +
libs/cli/pyproject.toml | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py
index 4de7568ea..770a406c6 100644
--- a/libs/cli/langgraph_cli/cli.py
+++ b/libs/cli/langgraph_cli/cli.py
@@ -609,6 +609,7 @@ def dev(
n_jobs_per_worker=n_jobs_per_worker,
open_browser=not no_browser,
debug_port=debug_port,
+ env=config_json.get("env", None),
)
diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml
index 63f6f8d9a..a271a073b 100644
--- a/libs/cli/pyproject.toml
+++ b/libs/cli/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
-version = "0.1.57"
+version = "0.1.58"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
From 55593446f80ac2a02d18dcee3c60a3a67131bb48 Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Thu, 21 Nov 2024 17:41:03 -0500
Subject: [PATCH 022/149] docs: get started with langgraph platform (#2469)
---
docs/docs/cloud/quick_start.md | 6 +-
docs/docs/concepts/index.md | 2 +-
docs/docs/tutorials/index.md | 22 +-
docs/docs/tutorials/introduction.ipynb | 77 +++---
.../langgraph-platform/local-server.md | 244 ++++++++++++++++++
.../rag/langgraph_adaptive_rag.ipynb | 2 +-
docs/mkdocs.yml | 3 +-
7 files changed, 307 insertions(+), 49 deletions(-)
create mode 100644 docs/docs/tutorials/langgraph-platform/local-server.md
diff --git a/docs/docs/cloud/quick_start.md b/docs/docs/cloud/quick_start.md
index 158bb1fc8..9f142c0bf 100644
--- a/docs/docs/cloud/quick_start.md
+++ b/docs/docs/cloud/quick_start.md
@@ -8,9 +8,9 @@ If you want to learn how to build an agent like this from scratch, take a look a
This tutorial will use:
-- Anthropic for the LLM - sign up and get an API key [here](https://console.anthropic.com/)
-- Tavily for the search engine - sign up and get an API key [here](https://app.tavily.com/)
-- LangSmith for hosting - sign up and get an API key [here](https://smith.langchain.com/)
+- Anthropic for the LLM - sign up and get an API key [here](https://console.anthropic.com/).
+- Tavily for the search engine - sign up and get an API key [here](https://app.tavily.com/).
+- LangSmith for hosting - sign up and get an API key [here](https://smith.langchain.com/).
## Create and configure your app
diff --git a/docs/docs/concepts/index.md b/docs/docs/concepts/index.md
index 10b4f0009..6c057c672 100644
--- a/docs/docs/concepts/index.md
+++ b/docs/docs/concepts/index.md
@@ -30,7 +30,7 @@ The conceptual guide does not cover step-by-step instructions or specific implem
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
- [FAQ](faq.md): Frequently asked questions about LangGraph.
-## LangGraph Platform
+## LangGraph Platform
LangGraph Platform is a commercial solution for deploying agentic applications in production, built on the open-source LangGraph framework.
diff --git a/docs/docs/tutorials/index.md b/docs/docs/tutorials/index.md
index 887740e6a..d9593c9b8 100644
--- a/docs/docs/tutorials/index.md
+++ b/docs/docs/tutorials/index.md
@@ -6,25 +6,23 @@ title: Tutorials
# Tutorials
-Welcome to the LangGraph Tutorials! These notebooks introduce LangGraph through building various language agents and applications.
+New to LangGraph or LLM app development? Read this material to get up and running building your first applications.
-## Quick Start
+## Get Started 🚀 {#quick-start}
-Learn the basics of LangGraph through a comprehensive quick start in which you will build an agent from scratch.
+- [LangGraph Quickstart](introduction.ipynb): Build a chatbot that can use tools and keep track of conversation history. Add human-in-the-loop capabilities and explore how time-travel works.
+- [LangGraph Server Quickstart](langgraph-platform/local-server.md): Launch a LangGraph server locally and interact with it using the REST API and LangGraph Studio Web UI.
+- [LangGraph Cloud QuickStart](../cloud/quick_start.md): Deploy a LangGraph app using LangGraph Cloud.
-- [Quick Start](introduction.ipynb): In this tutorial, you will build a support chatbot using LangGraph.
-- [LangGraph Cloud Quick Start](../cloud/quick_start.md): In this tutorial, you will build and deploy an agent to LangGraph Cloud.
+## Use cases 🛠️
-## Use cases
-
-Learn from example implementations of graphs designed for specific scenarios and that implement common design patterns.
+Explore practical implementations tailored for specific scenarios:
### Chatbots
-- [Customer Support](customer-support/customer-support.ipynb): Build a customer support chatbot to manage flights, hotel reservations, car rentals, and other tasks
-- [Prompt Generation from User Requirements](chatbots/information-gather-prompting.ipynb): Build an information gathering chatbot
-- [Code Assistant](code_assistant/langgraph_code_assistant.ipynb): Build a code analysis and generation assistant
-
+- [Customer Support](customer-support/customer-support.ipynb): Build a multi-functional support bot for flights, hotels, and car rentals.
+- [Prompt Generation from User Requirements](chatbots/information-gather-prompting.ipynb): Build an information gathering chatbot.
+- [Code Assistant](code_assistant/langgraph_code_assistant.ipynb): Build a code analysis and generation assistant.
### RAG
diff --git a/docs/docs/tutorials/introduction.ipynb b/docs/docs/tutorials/introduction.ipynb
index f37fcf138..712fbc28f 100644
--- a/docs/docs/tutorials/introduction.ipynb
+++ b/docs/docs/tutorials/introduction.ipynb
@@ -5,17 +5,17 @@
"id": "4a1aae78-88a6-4133-b905-7e46c8e3772f",
"metadata": {},
"source": [
- "# LangGraph Quick Start\n",
+ "# 🚀 LangGraph Quick Start\n",
"\n",
- "In this comprehensive quick start, we will build a support chatbot in LangGraph that can:\n",
+ "In this tutorial, we will build a support chatbot in LangGraph that can:\n",
"\n",
- "- Answer common questions by searching the web\n",
- "- Maintain conversation state across calls\n",
- "- Route complex queries to a human for review\n",
- "- Use custom state to control its behavior\n",
- "- Rewind and explore alternative conversation paths\n",
+ "✅ **Answer common questions** by searching the web \n",
+ "✅ **Maintain conversation state** across calls \n",
+ "✅ **Route complex queries** to a human for review \n",
+ "✅ **Use custom state** to control its behavior \n",
+ "✅ **Rewind and explore** alternative conversation paths \n",
"\n",
- "We'll start with a basic chatbot and progressively add more sophisticated capabilities, introducing key LangGraph concepts along the way.\n",
+ "We'll start with a **basic chatbot** and progressively add more sophisticated capabilities, introducing key LangGraph concepts along the way. Let’s dive in! 🌟\n",
"\n",
"## Setup\n",
"\n",
@@ -38,7 +38,7 @@
"id": "a6d1e870-1bc0-4d44-86c0-96681ccf6113",
"metadata": {},
"source": [
- "Next, set your API keys:"
+ "In this tutorial, we'll be "
]
},
{
@@ -120,27 +120,24 @@
]
},
{
+ "attachments": {},
"cell_type": "markdown",
- "id": "31c755cd-8994-4867-bdff-96a55d7beae7",
+ "id": "c08c41da-0855-49d3-9a3d-b7eb94413367",
"metadata": {},
"source": [
- "\n",
- "
Note
\n",
- "
\n",
- " The first thing you do when you define a graph is define the State of the graph. The State consists of the schema of the graph as well as reducer functions which specify how to apply updates to the state. In our example State is a TypedDict with a single key: messages. The messages key is annotated with the add_messages reducer function, which tells LangGraph to append new messages to the existing list, rather than overwriting it. State keys without an annotation will be overwritten by each update, storing the most recent value. Check out this conceptual guide to learn more about state, reducers and other low-level concepts.\n",
- "
\n",
- "
"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "4137feed-746e-4c72-a34a-f7a699ad5dcf",
- "metadata": {},
- "source": [
- "So now our graph knows two things:\n",
+ "Our graph can now handle two key tasks:\n",
+ "\n",
+ "1. Each `node` can receive the current `State` as input and output an update to the state.\n",
+ "2. Updates to `messages` will be appended to the existing list rather than overwriting it, thanks to the prebuilt [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/?h=add+messages#add_messages) function used with the `Annotated` syntax.\n",
+ "\n",
+ "------\n",
+ "\n",
+ "!!! tip \"Concept\"\n",
+ "\n",
+ " When defining a graph, the first step is to define its `State`. The `State` includes the graph's schema and [reducer functions](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) that handle state updates. In our example, `State` is a `TypedDict` with one key: `messages`. The [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages) reducer function is used to append new messages to the list instead of overwriting it. Keys without a reducer annotation will overwrite previous values. Learn more about state, reducers, and related concepts in [this guide](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages).\n",
+ "\n",
+ "---------\n",
"\n",
- "1. Every `node` we define will receive the current `State` as input and return a value that updates that state.\n",
- "2. `messages` will be _appended_ to the current list, rather than directly overwritten. This is communicated via the prebuilt [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/?h=add+messages#add_messages) function in the `Annotated` syntax.\n",
"\n",
"Next, add a \"`chatbot`\" node. Nodes represent units of work. They are typically regular python functions."
]
@@ -365,7 +362,7 @@
"id": "f22c5d4a-3134-413c-81fe-dd9752fbeb66",
"metadata": {},
"source": [
- "## Part 2: Enhancing the Chatbot with Tools\n",
+ "## Part 2: 🛠️ Enhancing the Chatbot with Tools\n",
"\n",
"To handle queries our chatbot can't answer \"from memory\", we'll integrate a web search tool. Our bot can use this tool to find relevant information and provide better responses.\n",
"\n",
@@ -3136,11 +3133,29 @@
"id": "e584d57f-5aad-4507-815f-0b2e4b64b791",
"metadata": {},
"source": [
- "## Conclusion\n",
+ "## Next Steps\n",
"\n",
- "Congrats! You've completed the intro tutorial and built a chat bot in LangGraph that supports tool calling, persistent memory, human-in-the-loop interactivity, and even time-travel!\n",
+ "Take your journey further by exploring deployment and advanced features:\n",
"\n",
- "The [LangGraph documentation](https://langchain-ai.github.io/langgraph/) is a great resource for diving deeper into the library's capabilities."
+ "### Server Quickstart\n",
+ "\n",
+ "- **[LangGraph Server Quickstart](../langgraph-platform/local-server)**: Launch a LangGraph server locally and interact with it using the REST API and LangGraph Studio Web UI.\n",
+ "\n",
+ "### LangGraph Cloud\n",
+ "\n",
+ "- **[LangGraph Cloud QuickStart](../../cloud/quick_start)**: Deploy your LangGraph app using LangGraph Cloud.\n",
+ "\n",
+ "### LangGraph Framework\n",
+ "\n",
+ "- **[LangGraph Concepts](../../concepts)**: Learn the foundational concepts of LangGraph. \n",
+ "- **[LangGraph How-to Guides](../../how-tos)**: Guides for common tasks with LangGraph.\n",
+ "\n",
+ "### LangGraph Platform\n",
+ "\n",
+ "Expand your knowledge with these resources:\n",
+ "\n",
+ "- **[LangGraph Platform Concepts](../../concepts#langgraph-platform)**: Understand the foundational concepts of the LangGraph Platform. \n",
+ "- **[LangGraph Platform How-to Guides](../../how-tos#langgraph-platform)**: Guides for common tasks with LangGraph Platform. "
]
}
],
@@ -3160,7 +3175,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.9"
+ "version": "3.11.4"
}
},
"nbformat": 4,
diff --git a/docs/docs/tutorials/langgraph-platform/local-server.md b/docs/docs/tutorials/langgraph-platform/local-server.md
new file mode 100644
index 000000000..a41333921
--- /dev/null
+++ b/docs/docs/tutorials/langgraph-platform/local-server.md
@@ -0,0 +1,244 @@
+# Quick Start: Launch Local LangGraph Server
+
+This is a quick start guide to help you get a LangGraph app up and running locally.
+
+!!! info "Requirements"
+
+ - [LangGraph CLI](https://langchain-ai.github.io/langgraph/cloud/reference/cli/): Requires langchain-cli[inmem] >= 0.1.58
+
+## Install the LangGraph CLI
+
+```bash
+pip install "langgraph-cli[inmem]==0.1.58" python-dot-env
+```
+
+## 🌱 Create a LangGraph App
+
+Create a new app from the `react-agent` template. This template is a simple agent that can be flexibly extended to many tools.
+
+=== "Python Server"
+
+ ```shell
+ langgraph new path/to/your/app --template react-agent-python
+ ```
+
+=== "Node Server"
+
+ ```shell
+ langgraph new path/to/your/app --template react-agent-js
+ ```
+
+!!! tip "Additional Templates"
+
+ If you use `langgraph new` without specifying a template, you will be presented with an interactive menu that will allow you to choose from a list of available templates.
+
+## Create a `.env` file
+
+You will find a `.env.example` in the root of your new LangGraph app. Create
+a `.env` file in the root of your new LangGraph app and copy the contents of the `.env.example` file into it, filling in the necessary API keys:
+
+```bash
+LANGSMITH_API_KEY=lsv2...
+TAVILY_API_KEY=tvly-...
+ANTHROPIC_API_KEY=sk-
+OPENAI_API_KEY=sk-...
+```
+
+Get API Keys
+
+
+
+## 🚀 Launch LangGraph Server
+
+```shell
+langgraph dev
+```
+
+This will start up the LangGraph API server locally. If this runs successfully, you should see something like:
+
+> Ready!
+>
+> - API: [http://localhost:8123](http://localhost:8123/)
+>
+> - Docs: http://localhost:8123/docs
+>
+> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:8123
+
+
+!!! note "In-Memory Mode"
+
+ The `langgraph dev` command starts LangGraph Server in an in-memory mode. This mode is suitable for development and testing purposes. For production use, you should deploy LangGraph Server with access to a persistent storage backend.
+
+ If you want to test your application with a persistent storage backend, you can use the `langgraph up` command instead of `langgraph dev`. You will
+ need to have `docker` installed on your machine to use this command.
+
+## LangGraph Studio Web UI
+
+Test your graph in the LangGraph Studio Web UI by visiting the URL provided in the output of the `langgraph up` command.
+
+> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:8123
+
+!!! warning "Safari Compatibility"
+
+ Currently, LangGraph Studio Web does not support Safari when running a server locally.
+
+## Test the API
+
+=== "Python SDK (Async)"
+
+ **Install the LangGraph Python SDK**
+
+ ```shell
+ pip install langgraph-sdk
+ ```
+
+ **Send a message to the assistant (threadless run)**
+
+ ```python
+ from langgraph_sdk import get_client
+
+ client = get_client(url="http://localhost:8123")
+
+ async for chunk in client.runs.stream(
+ None, # Threadless run
+ "agent", # Name of assistant. Defined in langgraph.json.
+ input={
+ "messages": [{
+ "role": "human",
+ "content": "What is LangGraph?",
+ }],
+ },
+ stream_mode="updates",
+ ):
+ print(f"Receiving new event of type: {chunk.event}...")
+ print(chunk.data)
+ print("\n\n")
+ ```
+
+=== "Python SDK (Sync)"
+
+ **Install the LangGraph Python SDK**
+
+ ```shell
+ pip install langgraph-sdk
+ ```
+
+ **Send a message to the assistant (threadless run)**
+
+ ```python
+ from langgraph_sdk import get_sync_client
+
+ client = get_sync_client(url="http://localhost:8123")
+
+ for chunk in client.runs.stream(
+ None, # Threadless run
+ "agent", # Name of assistant. Defined in langgraph.json.
+ input={
+ "messages": [{
+ "role": "human",
+ "content": "What is LangGraph?",
+ }],
+ },
+ stream_mode="updates",
+ ):
+ print(f"Receiving new event of type: {chunk.event}...")
+ print(chunk.data)
+ print("\n\n")
+ ```
+
+=== "Javascript SDK"
+
+ **Install the LangGraph JS SDK**
+
+ ```shell
+ npm install @langchain/langgraph-sdk
+ ```
+
+ **Send a message to the assistant (threadless run)**
+
+ ```js
+ const { Client } = await import("@langchain/langgraph-sdk");
+
+ // only set the apiUrl if you changed the default port when calling langgraph up
+ const client = new Client({ apiUrl: "http://localhost:8123"});
+
+ const streamResponse = client.runs.stream(
+ null, // Threadless run
+ "agent", // Assistant ID
+ {
+ input: {
+ "messages": [
+ { "role": "user", "content": "What is LangGraph?"}
+ ]
+ },
+ streamMode: "messages",
+ }
+ );
+
+ for await (const chunk of streamResponse) {
+ console.log(`Receiving new event of type: ${chunk.event}...`);
+ console.log(JSON.stringify(chunk.data));
+ console.log("\n\n");
+ }
+ ```
+
+=== "Rest API"
+
+ ```bash
+ curl -s --request POST \
+ --url "http://localhost:8123/runs/stream" \
+ --header 'Content-Type: application/json' \
+ --data "{
+ \"assistant_id\": \"agent\",
+ \"input\": {
+ \"messages\": [
+ {
+ \"role\": \"human\",
+ \"content\": \"What is LangGraph?\"
+ }
+ ]
+ },
+ \"stream_mode\": \"updates\"
+ }"
+ ```
+
+!!! tip "Auth"
+
+ If you're connecting to a remote server, you will need to provide a LangSmith
+ API Key for authorization. Please see the API Reference for the clients
+ for more information.
+
+## Next Steps
+
+Now that you have a LangGraph app running locally, take your journey further by exploring deployment and advanced features:
+
+### 🌐 Deploy to LangGraph Cloud
+
+- **[LangGraph Cloud QuickStart](../../cloud/quick_start.md)**: Deploy your LangGraph app using LangGraph Cloud.
+
+### 📚 Learn More about LangGraph Platform
+
+Expand your knowledge with these resources:
+
+- **[LangGraph Platform Concepts](../../concepts/index.md#langgraph-platform)**: Understand the foundational concepts of the LangGraph Platform.
+- **[LangGraph Platform How-to Guides](../../how-tos/index.md#langgraph-platform)**: Discover step-by-step guides to build and deploy applications.
+
+### 🛠️ Developer References
+
+Access detailed documentation for development and API usage:
+
+- **[LangGraph Server API Reference](../../cloud/reference/api/api_ref.html)**: Explore the LangGraph Server API documentation.
+- **[Python SDK Reference](../../cloud/reference/sdk/python_sdk_ref.md)**: Explore the Python SDK API Reference.
+- **[JS/TS SDK Reference](../../cloud/reference/sdk/js_ts_sdk_ref.md)**: Explore the Python SDK API Reference.
\ No newline at end of file
diff --git a/docs/docs/tutorials/rag/langgraph_adaptive_rag.ipynb b/docs/docs/tutorials/rag/langgraph_adaptive_rag.ipynb
index f4a887ec9..8729822dc 100644
--- a/docs/docs/tutorials/rag/langgraph_adaptive_rag.ipynb
+++ b/docs/docs/tutorials/rag/langgraph_adaptive_rag.ipynb
@@ -934,7 +934,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.9"
+ "version": "3.11.4"
}
},
"nbformat": 4,
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 648f8618e..3167d5520 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -94,6 +94,7 @@ nav:
- Quick Start:
- Quick Start: tutorials#quick-start
- tutorials/introduction.ipynb
+ - tutorials/langgraph-platform/local-server.md
- cloud/quick_start.md
- Chatbots:
- Chatbots: tutorials#chatbots
@@ -438,4 +439,4 @@ validation:
# and those anchors are not available in the actual doc
anchors: info
# this is needed to handle headers with anchors for nav
- not_found: info
\ No newline at end of file
+ not_found: info
From 26ce731eab0b8497fde06560ed6747b314645ee9 Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Thu, 21 Nov 2024 17:48:12 -0500
Subject: [PATCH 023/149] docs: update README.md (#2474)
---
README.md | 2 +-
libs/langgraph/README.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 6f7b62676..a05b4ce97 100644
--- a/README.md
+++ b/README.md
@@ -238,7 +238,7 @@ final_state["messages"][-1].content
* [How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): Accomplish specific things within LangGraph, from streaming, to adding memory & persistence, to common design patterns (branching, subgraphs, etc.), these are the place to go if you want to copy and run a specific code snippet.
* [Conceptual Guides](https://langchain-ai.github.io/langgraph/concepts/high_level/): In-depth explanations of the key concepts and principles behind LangGraph, such as nodes, edges, state and more.
* [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Review important classes and methods, simple examples of how to use the graph and checkpointing APIs, higher-level prebuilt components and more.
-* [Cloud (beta)](https://langchain-ai.github.io/langgraph/cloud/): With one click, deploy LangGraph applications to LangGraph Cloud.
+* [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform): LangGraph Platform is a commercial solution for deploying agentic applications in production, built on the open-source LangGraph framework.
## Contributing
diff --git a/libs/langgraph/README.md b/libs/langgraph/README.md
index 6f7b62676..a05b4ce97 100644
--- a/libs/langgraph/README.md
+++ b/libs/langgraph/README.md
@@ -238,7 +238,7 @@ final_state["messages"][-1].content
* [How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): Accomplish specific things within LangGraph, from streaming, to adding memory & persistence, to common design patterns (branching, subgraphs, etc.), these are the place to go if you want to copy and run a specific code snippet.
* [Conceptual Guides](https://langchain-ai.github.io/langgraph/concepts/high_level/): In-depth explanations of the key concepts and principles behind LangGraph, such as nodes, edges, state and more.
* [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Review important classes and methods, simple examples of how to use the graph and checkpointing APIs, higher-level prebuilt components and more.
-* [Cloud (beta)](https://langchain-ai.github.io/langgraph/cloud/): With one click, deploy LangGraph applications to LangGraph Cloud.
+* [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform): LangGraph Platform is a commercial solution for deploying agentic applications in production, built on the open-source LangGraph framework.
## Contributing
From b09e7b20b05f288ba120c2b8031c79cdc1d40637 Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Thu, 21 Nov 2024 18:28:24 -0500
Subject: [PATCH 024/149] docs: do not check localhost links (#2505)
---
.github/workflows/deploy_docs.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml
index 721bb4117..ad973533a 100644
--- a/.github/workflows/deploy_docs.yml
+++ b/.github/workflows/deploy_docs.yml
@@ -88,6 +88,7 @@ jobs:
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
--check-links-ignore "https://x.com/.*" \
--check-links-ignore "https://github\.com/.*" \
+ --check-links-ignore "http://localhost\.com.*" \ # Include ports
--check-links-ignore "/.*\.(ipynb|html)$" \
--check-links-ignore "https://python\.langchain\.com/.*" \
--check-links-ignore "https://openai\.com/.*" \
@@ -104,6 +105,7 @@ jobs:
echo "Running link check on HTML files matching changed notebook files..."
poetry run pytest -v \
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
+ --check-links-ignore "http://localhost\.com.*" \ # Include ports
--check-links-ignore "https://x.com/.*" \
--check-links-ignore "https://github\.com/.*" \
--check-links-ignore "/.*\.(ipynb|html)$" \
From 3eedeac0d4d2cdce5fdaf5431083ffe4635502f0 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Thu, 21 Nov 2024 15:31:45 -0800
Subject: [PATCH 025/149] Not red
---
libs/cli/langgraph_cli/cli.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py
index 770a406c6..07c894fac 100644
--- a/libs/cli/langgraph_cli/cli.py
+++ b/libs/cli/langgraph_cli/cli.py
@@ -189,7 +189,6 @@ def up(
click.secho(
"""For local dev, requires env var LANGSMITH_API_KEY with access to LangGraph Cloud closed beta.
For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KEY.""",
- fg="red",
)
with Runner() as runner, Progress(message="Pulling...") as set:
capabilities = langgraph_cli.docker.check_capabilities(runner)
From aeb6f784e131fda817eadf793e4fdc6cef214736 Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Thu, 21 Nov 2024 21:21:30 -0500
Subject: [PATCH 026/149] docs: fix link checking? (#2506)
---
.github/workflows/deploy_docs.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml
index ad973533a..2a3544874 100644
--- a/.github/workflows/deploy_docs.yml
+++ b/.github/workflows/deploy_docs.yml
@@ -88,7 +88,7 @@ jobs:
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
--check-links-ignore "https://x.com/.*" \
--check-links-ignore "https://github\.com/.*" \
- --check-links-ignore "http://localhost\.com.*" \ # Include ports
+ --check-links-ignore "http://localhost\.com.*" \
--check-links-ignore "/.*\.(ipynb|html)$" \
--check-links-ignore "https://python\.langchain\.com/.*" \
--check-links-ignore "https://openai\.com/.*" \
@@ -105,7 +105,7 @@ jobs:
echo "Running link check on HTML files matching changed notebook files..."
poetry run pytest -v \
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
- --check-links-ignore "http://localhost\.com.*" \ # Include ports
+ --check-links-ignore "http://localhost\.com.*" \
--check-links-ignore "https://x.com/.*" \
--check-links-ignore "https://github\.com/.*" \
--check-links-ignore "/.*\.(ipynb|html)$" \
From 93b8525dc1ae103557ab148811f8e6d4def73712 Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Thu, 21 Nov 2024 21:59:43 -0500
Subject: [PATCH 027/149] docs: fix link checker (#2508)
3rd attempt to fix localhost link
---
.github/workflows/deploy_docs.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml
index 2a3544874..6c6a169fa 100644
--- a/.github/workflows/deploy_docs.yml
+++ b/.github/workflows/deploy_docs.yml
@@ -88,7 +88,7 @@ jobs:
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
--check-links-ignore "https://x.com/.*" \
--check-links-ignore "https://github\.com/.*" \
- --check-links-ignore "http://localhost\.com.*" \
+ --check-links-ignore "http://localhost:8123/.*" \
--check-links-ignore "/.*\.(ipynb|html)$" \
--check-links-ignore "https://python\.langchain\.com/.*" \
--check-links-ignore "https://openai\.com/.*" \
@@ -105,7 +105,7 @@ jobs:
echo "Running link check on HTML files matching changed notebook files..."
poetry run pytest -v \
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
- --check-links-ignore "http://localhost\.com.*" \
+ --check-links-ignore "http://localhost:8123/.*" \
--check-links-ignore "https://x.com/.*" \
--check-links-ignore "https://github\.com/.*" \
--check-links-ignore "/.*\.(ipynb|html)$" \
From 0d0665a6e39833575278c54f7fd12705ae989d49 Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Fri, 22 Nov 2024 11:56:16 -0500
Subject: [PATCH 028/149] docs: Add resource allocation (#2511)
---
docs/docs/concepts/langgraph_cloud.md | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/docs/docs/concepts/langgraph_cloud.md b/docs/docs/concepts/langgraph_cloud.md
index 371e8bb47..11e5c8afc 100644
--- a/docs/docs/concepts/langgraph_cloud.md
+++ b/docs/docs/concepts/langgraph_cloud.md
@@ -14,6 +14,13 @@ A **deployment** is an instance of a LangGraph API. A single deployment can have
See the [how-to guide](../cloud/deployment/cloud.md#create-new-deployment) for creating a new deployment.
+## Resource Allocation
+
+| **Deployment Type** | **CPU** | **Memory** | **Scaling** |
+|---------------------|---------|------------|---------------------|
+| Development | 1 CPU | 1 GB | Up to 1 container |
+| Production | 1 CPU | 2 GB | Up to 10 containers |
+
## Revision
A revision is an iteration of a [deployment](#deployment). When a new deployment is created, an initial revision is automatically created. To deploy new code changes or update environment variable configurations for a deployment, a new revision must be created. When a revision is created, a new container image is built automatically.
@@ -33,6 +40,7 @@ A high-level diagram of a Cloud SaaS deployment.

+
## Related
- [Deployment Options](./deployment_options.md)
From 65f515e020682986659eb31cbe4fc5c41269c0f6 Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Fri, 22 Nov 2024 12:17:36 -0500
Subject: [PATCH 029/149] docs: add helm chart link (#2512)
---
docs/docs/concepts/self_hosted.md | 6 +++++-
docs/docs/how-tos/deploy-self-hosted.md | 6 +++++-
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/docs/docs/concepts/self_hosted.md b/docs/docs/concepts/self_hosted.md
index ffa26a873..d2f236705 100644
--- a/docs/docs/concepts/self_hosted.md
+++ b/docs/docs/concepts/self_hosted.md
@@ -7,7 +7,7 @@
## Versions
-There are two versions of the self hosted deployment: [Self-Hosted Enterprise](./deployment_options.md#self-hosted-enterprise) and [Self-Hosted Lite](./deployment_options.md#self-hosted-lite).
+There are two versions of the self-hosted deployment: [Self-Hosted Enterprise](./deployment_options.md#self-hosted-enterprise) and [Self-Hosted Lite](./deployment_options.md#self-hosted-lite).
### Self-Hosted Lite
@@ -34,6 +34,10 @@ To use the Self-Hosted Enterprise version, you must acquire a license key that y
For step-by-step instructions, see [How to set up a self-hosted deployment of LangGraph](../how-tos/deploy-self-hosted.md).
+## Helm Chart
+
+If you would like to deploy LangGraph Cloud on Kubernetes, you can use this [Helm chart](https://github.com/langchain-ai/helm/blob/main/charts/langgraph-cloud/README.md).
+
## Related
- [How to set up a self-hosted deployment of LangGraph](../how-tos/deploy-self-hosted.md).
diff --git a/docs/docs/how-tos/deploy-self-hosted.md b/docs/docs/how-tos/deploy-self-hosted.md
index 5e1bf93e4..88d7c72e4 100644
--- a/docs/docs/how-tos/deploy-self-hosted.md
+++ b/docs/docs/how-tos/deploy-self-hosted.md
@@ -17,6 +17,10 @@ You will need to do the following:
2. Build a docker image with the [LangGraph Server](../concepts/langgraph_server.md) using the [LangGraph CLI](../concepts/langgraph_cli.md).
3. Deploy a web server that will run the docker image and pass in the necessary environment variables.
+## Helm Chart
+
+If you would like to deploy LangGraph Cloud on Kubernetes, you can use this [Helm chart](https://github.com/langchain-ai/helm/blob/main/charts/langgraph-cloud/README.md).
+
## Environment Variables
You will eventually need to pass in the following environment variables to the LangGraph Deploy server:
@@ -70,7 +74,7 @@ If you want to run this quickly without setting up a separate Redis and Postgres
* You need to replace `my-image` with the name of the image you built in the previous step (from `langgraph build`).
and you should provide appropriate values for `REDIS_URI`, `DATABASE_URI`, and `LANGSMITH_API_KEY`.
* If your application requires additional environment variables, you can pass them in a similar way.
- * If using [Self-Hosted Enterprise](../concepts/deployment_options.md#self-hosted-enterprise), you must provide `LANGGRAPH_CLOUD_LICENSE_KEY` as an additional environment variable.
+ * If using [Self-Hosted Enterprise](../concepts/deployment_options.md#self-hosted-enterprise, you must provide `LANGGRAPH_CLOUD_LICENSE_KEY` as an additional environment variable.
### Using Docker Compose
From 416dfe95da68065b26e755c93b2bd1433c5ea9f6 Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Fri, 22 Nov 2024 13:16:41 -0500
Subject: [PATCH 030/149] qxqx
---
docs/docs/concepts/template_applications.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/docs/concepts/template_applications.md b/docs/docs/concepts/template_applications.md
index df8bc5e63..621755961 100644
--- a/docs/docs/concepts/template_applications.md
+++ b/docs/docs/concepts/template_applications.md
@@ -6,7 +6,8 @@
Templates are open source reference applications designed to help you get started quickly when building with LangGraph. They provide working examples of common agentic workflows that can be customized to your needs.
-Templates can be accessed via [LangGraph Studio (macOS only)](langgraph_studio.md), or cloned directly from Github. You can download LangGraph Studio and see available templates [here](https://studio.langchain.com/).
+You can create an application from a template using the LangGraph CLI.
+
## Available templates
From 05791f5dfc5804a5022549cf10a60d86568da2de Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Fri, 22 Nov 2024 13:26:46 -0500
Subject: [PATCH 031/149] qxqx
---
docs/docs/concepts/template_applications.md | 27 ++++++++++++++++++---
docs/docs/tutorials/index.md | 1 +
2 files changed, 24 insertions(+), 4 deletions(-)
diff --git a/docs/docs/concepts/template_applications.md b/docs/docs/concepts/template_applications.md
index 621755961..366237e1d 100644
--- a/docs/docs/concepts/template_applications.md
+++ b/docs/docs/concepts/template_applications.md
@@ -1,9 +1,5 @@
# Template Applications
-!!! note Prerequisites
-
- - [LangGraph Studio](./langgraph_studio.md)
-
Templates are open source reference applications designed to help you get started quickly when building with LangGraph. They provide working examples of common agentic workflows that can be customized to your needs.
You can create an application from a template using the LangGraph CLI.
@@ -18,3 +14,26 @@ You can create an application from a template using the LangGraph CLI.
| **Memory Agent** | A ReAct-style agent with an additional tool to store memories for use across threads. | [Repo](https://github.com/langchain-ai/memory-agent) | [Repo](https://github.com/langchain-ai/memory-agent-js) |
| **Retrieval Agent** | An agent that includes a retrieval-based question-answering system. | [Repo](https://github.com/langchain-ai/retrieval-agent-template) | [Repo](https://github.com/langchain-ai/retrieval-agent-template-js) |
| **Data-Enrichment Agent** | An agent that performs web searches and organizes its findings into a structured format. | [Repo](https://github.com/langchain-ai/data-enrichment) | [Repo](https://github.com/langchain-ai/data-enrichment-js) |
+
+
+
+## 🌱 Create a LangGraph App
+
+To create a new app from a template, use the `langgraph new` command. This command will create a new directory with the specified template.
+
+```shell
+
+This is a quick start guide to help you get a LangGraph app up and running locally.
+
+!!! info "Requirements"
+
+ - [LangGraph CLI](https://langchain-ai.github.io/langgraph/cloud/reference/cli/): Requires langchain-cli[inmem] >= 0.1.58
+
+## Install the LangGraph CLI
+
+```bash
+pip install "langgraph-cli[inmem]==0.1.58" python-dot-env
+```
+
+
+
diff --git a/docs/docs/tutorials/index.md b/docs/docs/tutorials/index.md
index d9593c9b8..2605c282e 100644
--- a/docs/docs/tutorials/index.md
+++ b/docs/docs/tutorials/index.md
@@ -13,6 +13,7 @@ New to LangGraph or LLM app development? Read this material to get up and runnin
- [LangGraph Quickstart](introduction.ipynb): Build a chatbot that can use tools and keep track of conversation history. Add human-in-the-loop capabilities and explore how time-travel works.
- [LangGraph Server Quickstart](langgraph-platform/local-server.md): Launch a LangGraph server locally and interact with it using the REST API and LangGraph Studio Web UI.
- [LangGraph Cloud QuickStart](../cloud/quick_start.md): Deploy a LangGraph app using LangGraph Cloud.
+- [Start from a ](../concepts/template_applications.md): Use a template to quickly create a new LangGraph application.
## Use cases 🛠️
From b4900341e40a4dde6ebef740a2df262b04064213 Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Fri, 22 Nov 2024 14:07:54 -0500
Subject: [PATCH 032/149] docs: fix broken link (#2514)
We need to check later why CI didn't fail with original PR that broke
the link
---
docs/docs/how-tos/deploy-self-hosted.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/docs/how-tos/deploy-self-hosted.md b/docs/docs/how-tos/deploy-self-hosted.md
index 88d7c72e4..d0d3a3efe 100644
--- a/docs/docs/how-tos/deploy-self-hosted.md
+++ b/docs/docs/how-tos/deploy-self-hosted.md
@@ -74,7 +74,7 @@ If you want to run this quickly without setting up a separate Redis and Postgres
* You need to replace `my-image` with the name of the image you built in the previous step (from `langgraph build`).
and you should provide appropriate values for `REDIS_URI`, `DATABASE_URI`, and `LANGSMITH_API_KEY`.
* If your application requires additional environment variables, you can pass them in a similar way.
- * If using [Self-Hosted Enterprise](../concepts/deployment_options.md#self-hosted-enterprise, you must provide `LANGGRAPH_CLOUD_LICENSE_KEY` as an additional environment variable.
+ * If using [Self-Hosted Enterprise](../concepts/deployment_options.md#self-hosted-enterprise), you must provide `LANGGRAPH_CLOUD_LICENSE_KEY` as an additional environment variable.
### Using Docker Compose
From 3351d4f6c5c3d5fcb048fd9d6f45bb52a2c57a4d Mon Sep 17 00:00:00 2001
From: Mingqi Hu
Date: Sat, 23 Nov 2024 03:17:59 +0800
Subject: [PATCH 033/149] docs: fix typo (#2510)
`python-dotenv` not `python-dot-env`
Signed-off-by: Mingqi
Co-authored-by: Eugene Yurtsev
---
docs/docs/tutorials/langgraph-platform/local-server.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/docs/tutorials/langgraph-platform/local-server.md b/docs/docs/tutorials/langgraph-platform/local-server.md
index a41333921..5778782e4 100644
--- a/docs/docs/tutorials/langgraph-platform/local-server.md
+++ b/docs/docs/tutorials/langgraph-platform/local-server.md
@@ -9,7 +9,7 @@ This is a quick start guide to help you get a LangGraph app up and running local
## Install the LangGraph CLI
```bash
-pip install "langgraph-cli[inmem]==0.1.58" python-dot-env
+pip install "langgraph-cli[inmem]==0.1.58" python-dotenv
```
## 🌱 Create a LangGraph App
From f122ae2eb16d2bd36b6518e1008fb48fa4f79004 Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Fri, 22 Nov 2024 14:20:56 -0500
Subject: [PATCH 034/149] qxqx
---
docs/docs/how-tos/deploy-self-hosted.md | 2 +-
docs/docs/tutorials/langgraph-platform/local-server.md | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/docs/how-tos/deploy-self-hosted.md b/docs/docs/how-tos/deploy-self-hosted.md
index 88d7c72e4..d0d3a3efe 100644
--- a/docs/docs/how-tos/deploy-self-hosted.md
+++ b/docs/docs/how-tos/deploy-self-hosted.md
@@ -74,7 +74,7 @@ If you want to run this quickly without setting up a separate Redis and Postgres
* You need to replace `my-image` with the name of the image you built in the previous step (from `langgraph build`).
and you should provide appropriate values for `REDIS_URI`, `DATABASE_URI`, and `LANGSMITH_API_KEY`.
* If your application requires additional environment variables, you can pass them in a similar way.
- * If using [Self-Hosted Enterprise](../concepts/deployment_options.md#self-hosted-enterprise, you must provide `LANGGRAPH_CLOUD_LICENSE_KEY` as an additional environment variable.
+ * If using [Self-Hosted Enterprise](../concepts/deployment_options.md#self-hosted-enterprise), you must provide `LANGGRAPH_CLOUD_LICENSE_KEY` as an additional environment variable.
### Using Docker Compose
diff --git a/docs/docs/tutorials/langgraph-platform/local-server.md b/docs/docs/tutorials/langgraph-platform/local-server.md
index a41333921..22754c471 100644
--- a/docs/docs/tutorials/langgraph-platform/local-server.md
+++ b/docs/docs/tutorials/langgraph-platform/local-server.md
@@ -9,7 +9,7 @@ This is a quick start guide to help you get a LangGraph app up and running local
## Install the LangGraph CLI
```bash
-pip install "langgraph-cli[inmem]==0.1.58" python-dot-env
+pip install "langgraph-cli[inmem]==0.1.58" python-dotenv
```
## 🌱 Create a LangGraph App
@@ -241,4 +241,4 @@ Access detailed documentation for development and API usage:
- **[LangGraph Server API Reference](../../cloud/reference/api/api_ref.html)**: Explore the LangGraph Server API documentation.
- **[Python SDK Reference](../../cloud/reference/sdk/python_sdk_ref.md)**: Explore the Python SDK API Reference.
-- **[JS/TS SDK Reference](../../cloud/reference/sdk/js_ts_sdk_ref.md)**: Explore the Python SDK API Reference.
\ No newline at end of file
+- **[JS/TS SDK Reference](../../cloud/reference/sdk/js_ts_sdk_ref.md)**: Explore the Python SDK API Reference.
From c1c2ce8f1be240f02b6deec1d508be550124e4f2 Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Fri, 22 Nov 2024 14:42:36 -0500
Subject: [PATCH 035/149] x
---
docs/docs/concepts/template_applications.md | 51 +++++++++++++++------
docs/docs/tutorials/index.md | 2 +-
2 files changed, 38 insertions(+), 15 deletions(-)
diff --git a/docs/docs/concepts/template_applications.md b/docs/docs/concepts/template_applications.md
index 366237e1d..1bed86a32 100644
--- a/docs/docs/concepts/template_applications.md
+++ b/docs/docs/concepts/template_applications.md
@@ -4,8 +4,18 @@ Templates are open source reference applications designed to help you get starte
You can create an application from a template using the LangGraph CLI.
+!!! info "Requirements"
-## Available templates
+ - Python >= 3.11
+ - [LangGraph CLI](https://langchain-ai.github.io/langgraph/cloud/reference/cli/): Requires langchain-cli[inmem] >= 0.1.58
+
+## Install the LangGraph CLI
+
+```bash
+pip install "langgraph-cli[inmem]==0.1.58" python-dotenv
+```
+
+## Available Templates
| Template | Description | Python | JS/TS |
|---------------------------|------------------------------------------------------------------------------------------|------------------------------------------------------------------|---------------------------------------------------------------------|
@@ -16,24 +26,37 @@ You can create an application from a template using the LangGraph CLI.
| **Data-Enrichment Agent** | An agent that performs web searches and organizes its findings into a structured format. | [Repo](https://github.com/langchain-ai/data-enrichment) | [Repo](https://github.com/langchain-ai/data-enrichment-js) |
-
## 🌱 Create a LangGraph App
-To create a new app from a template, use the `langgraph new` command. This command will create a new directory with the specified template.
-
-```shell
-
-This is a quick start guide to help you get a LangGraph app up and running locally.
-
-!!! info "Requirements"
-
- - [LangGraph CLI](https://langchain-ai.github.io/langgraph/cloud/reference/cli/): Requires langchain-cli[inmem] >= 0.1.58
-
-## Install the LangGraph CLI
+To create a new app from a template, use the `langgraph new` command.
```bash
-pip install "langgraph-cli[inmem]==0.1.58" python-dot-env
+langgraph new
```
+## Next Steps
+Review the `README.md` file in the root of your new LangGraph app for more information about the template and how to customize it.
+After configuring the app properly and adding your API keys, you can start the app using the LangGraph CLI:
+
+```bash
+langgraph dev
+```
+
+See the following guides for more information on how to deploy your app:
+
+- **[Launch Local LangGraph Server](../tutorials/langgraph-platform/local-server.md)**: This quick start guide shows how to start a LangGraph Server locally for the **ReAct Agent** template. The steps are similar for other templates.
+- **[Deploy to LangGraph Cloud](../cloud/quick_start.md)**: Deploy your LangGraph app using LangGraph Cloud.
+
+### LangGraph Framework
+
+- **[LangGraph Concepts](../../concepts)**: Learn the foundational concepts of LangGraph.
+- **[LangGraph How-to Guides](../../how-tos)**: Guides for common tasks with LangGraph.
+
+### 📚 Learn More about LangGraph Platform
+
+Expand your knowledge with these resources:
+
+- **[LangGraph Platform Concepts](../concepts/index.md#langgraph-platform)**: Understand the foundational concepts of the LangGraph Platform.
+- **[LangGraph Platform How-to Guides](../how-tos/index.md#langgraph-platform)**: Discover step-by-step guides to build and deploy applications.
diff --git a/docs/docs/tutorials/index.md b/docs/docs/tutorials/index.md
index 2605c282e..286137479 100644
--- a/docs/docs/tutorials/index.md
+++ b/docs/docs/tutorials/index.md
@@ -13,7 +13,7 @@ New to LangGraph or LLM app development? Read this material to get up and runnin
- [LangGraph Quickstart](introduction.ipynb): Build a chatbot that can use tools and keep track of conversation history. Add human-in-the-loop capabilities and explore how time-travel works.
- [LangGraph Server Quickstart](langgraph-platform/local-server.md): Launch a LangGraph server locally and interact with it using the REST API and LangGraph Studio Web UI.
- [LangGraph Cloud QuickStart](../cloud/quick_start.md): Deploy a LangGraph app using LangGraph Cloud.
-- [Start from a ](../concepts/template_applications.md): Use a template to quickly create a new LangGraph application.
+- [LangGraph Template Quickstart](../concepts/template_applications.md): Quickly start building with LangGraph Platform using a template application.
## Use cases 🛠️
From 24b16908b712d2c1ce9cb705c731109908a401d0 Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Fri, 22 Nov 2024 14:43:08 -0500
Subject: [PATCH 036/149] x
---
docs/docs/concepts/template_applications.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/docs/concepts/template_applications.md b/docs/docs/concepts/template_applications.md
index 1bed86a32..34809eae1 100644
--- a/docs/docs/concepts/template_applications.md
+++ b/docs/docs/concepts/template_applications.md
@@ -51,8 +51,8 @@ See the following guides for more information on how to deploy your app:
### LangGraph Framework
-- **[LangGraph Concepts](../../concepts)**: Learn the foundational concepts of LangGraph.
-- **[LangGraph How-to Guides](../../how-tos)**: Guides for common tasks with LangGraph.
+- **[LangGraph Concepts](../concepts)**: Learn the foundational concepts of LangGraph.
+- **[LangGraph How-to Guides](../how-tos)**: Guides for common tasks with LangGraph.
### 📚 Learn More about LangGraph Platform
From f08155d60be7a07aadddbcc76ae7c49dbb762f65 Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Fri, 22 Nov 2024 14:43:28 -0500
Subject: [PATCH 037/149] x
---
docs/docs/concepts/template_applications.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/docs/concepts/template_applications.md b/docs/docs/concepts/template_applications.md
index 34809eae1..32209ad47 100644
--- a/docs/docs/concepts/template_applications.md
+++ b/docs/docs/concepts/template_applications.md
@@ -51,8 +51,8 @@ See the following guides for more information on how to deploy your app:
### LangGraph Framework
-- **[LangGraph Concepts](../concepts)**: Learn the foundational concepts of LangGraph.
-- **[LangGraph How-to Guides](../how-tos)**: Guides for common tasks with LangGraph.
+- **[LangGraph Concepts](../concepts/index.md)**: Learn the foundational concepts of LangGraph.
+- **[LangGraph How-to Guides](../how-tos/index.md)**: Guides for common tasks with LangGraph.
### 📚 Learn More about LangGraph Platform
From 4f4e7a69818d4f081400711c6382ce83b38d2d16 Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Fri, 22 Nov 2024 14:50:31 -0500
Subject: [PATCH 038/149] docs: more fixes for python version (#2515)
---
docs/docs/tutorials/langgraph-platform/local-server.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/docs/docs/tutorials/langgraph-platform/local-server.md b/docs/docs/tutorials/langgraph-platform/local-server.md
index 5778782e4..9c8e3ea9f 100644
--- a/docs/docs/tutorials/langgraph-platform/local-server.md
+++ b/docs/docs/tutorials/langgraph-platform/local-server.md
@@ -4,6 +4,7 @@ This is a quick start guide to help you get a LangGraph app up and running local
!!! info "Requirements"
+ - Python >= 3.11
- [LangGraph CLI](https://langchain-ai.github.io/langgraph/cloud/reference/cli/): Requires langchain-cli[inmem] >= 0.1.58
## Install the LangGraph CLI
@@ -32,6 +33,14 @@ Create a new app from the `react-agent` template. This template is a simple agen
If you use `langgraph new` without specifying a template, you will be presented with an interactive menu that will allow you to choose from a list of available templates.
+## Install Dependencies
+
+In the root of your new LangGraph app, install the dependencies:
+
+```shell
+pip install .
+```
+
## Create a `.env` file
You will find a `.env.example` in the root of your new LangGraph app. Create
From fed60e713cf2bd8e76fbdce01294706853dc28a7 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Fri, 22 Nov 2024 16:28:43 -0800
Subject: [PATCH 039/149] lib: Add Command(graph=Command.PARENT, ...)
- This makes the command bubble up out of the current graph and be handled by the calling graph (the immediate parent)
- This could be extended to support eg. ROOT graph, or some other level
---
libs/langgraph/langgraph/errors.py | 17 +++++++--
libs/langgraph/langgraph/graph/state.py | 15 +++++++-
.../langgraph/langgraph/prebuilt/tool_node.py | 6 +--
libs/langgraph/langgraph/pregel/algo.py | 2 +
libs/langgraph/langgraph/pregel/executor.py | 6 +--
libs/langgraph/langgraph/pregel/io.py | 3 ++
libs/langgraph/langgraph/pregel/retry.py | 38 +++++++++++++++++--
libs/langgraph/langgraph/pregel/runner.py | 6 +--
libs/langgraph/langgraph/types.py | 6 +++
9 files changed, 82 insertions(+), 17 deletions(-)
diff --git a/libs/langgraph/langgraph/errors.py b/libs/langgraph/langgraph/errors.py
index 2450b42b1..0737a31d0 100644
--- a/libs/langgraph/langgraph/errors.py
+++ b/libs/langgraph/langgraph/errors.py
@@ -2,7 +2,7 @@ from enum import Enum
from typing import Any, Sequence
from langgraph.checkpoint.base import EmptyChannelError # noqa: F401
-from langgraph.types import Interrupt
+from langgraph.types import Command, Interrupt
# EmptyChannelError re-exported for backwards compatibility
@@ -58,7 +58,11 @@ class InvalidUpdateError(Exception):
pass
-class GraphInterrupt(Exception):
+class GraphBubbleUp(Exception):
+ pass
+
+
+class GraphInterrupt(GraphBubbleUp):
"""Raised when a subgraph is interrupted, suppressed by the root graph.
Never raised directly, or surfaced to the user."""
@@ -73,13 +77,20 @@ class NodeInterrupt(GraphInterrupt):
super().__init__([Interrupt(value=value)])
-class GraphDelegate(Exception):
+class GraphDelegate(GraphBubbleUp):
"""Raised when a graph is delegated (for distributed mode)."""
def __init__(self, *args: dict[str, Any]) -> None:
super().__init__(*args)
+class ParentCommand(GraphBubbleUp):
+ args: tuple[Command]
+
+ def __init__(self, command: Command) -> None:
+ super().__init__(command)
+
+
class EmptyInputError(Exception):
"""Raised when graph receives an empty input."""
diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py
index cce742911..0684fb29c 100644
--- a/libs/langgraph/langgraph/graph/state.py
+++ b/libs/langgraph/langgraph/graph/state.py
@@ -37,7 +37,12 @@ from langgraph.channels.ephemeral_value import EphemeralValue
from langgraph.channels.last_value import LastValue
from langgraph.channels.named_barrier_value import NamedBarrierValue
from langgraph.constants import EMPTY_SEQ, NS_END, NS_SEP, SELF, TAG_HIDDEN
-from langgraph.errors import ErrorCode, InvalidUpdateError, create_error_message
+from langgraph.errors import (
+ ErrorCode,
+ InvalidUpdateError,
+ ParentCommand,
+ create_error_message,
+)
from langgraph.graph.graph import END, START, Branch, CompiledGraph, Graph, Send
from langgraph.managed.base import (
ChannelKeyPlaceholder,
@@ -623,6 +628,8 @@ class CompiledStateGraph(CompiledGraph):
def _get_root(input: Any) -> Any:
if isinstance(input, Command):
+ if input.graph == Command.PARENT:
+ return SKIP_WRITE
return input.update
else:
return input
@@ -640,6 +647,8 @@ class CompiledStateGraph(CompiledGraph):
)
return input.get(key, SKIP_WRITE)
elif isinstance(input, Command):
+ if input.graph == Command.PARENT:
+ return SKIP_WRITE
return _get_state_key(input.update, key=key)
elif get_type_hints(type(input)):
value = getattr(input, key, SKIP_WRITE)
@@ -822,6 +831,8 @@ def _control_branch(value: Any) -> Sequence[Union[str, Send]]:
return [value]
if not isinstance(value, GraphCommand):
return EMPTY_SEQ
+ if value.graph == Command.PARENT:
+ raise ParentCommand(value)
rtn: list[Union[str, Send]] = []
if isinstance(value.goto, str):
rtn.append(value.goto)
@@ -839,6 +850,8 @@ async def _acontrol_branch(value: Any) -> Sequence[Union[str, Send]]:
return [value]
if not isinstance(value, GraphCommand):
return EMPTY_SEQ
+ if value.graph == Command.PARENT:
+ raise ParentCommand(value)
rtn: list[Union[str, Send]] = []
if isinstance(value.goto, str):
rtn.append(value.goto)
diff --git a/libs/langgraph/langgraph/prebuilt/tool_node.py b/libs/langgraph/langgraph/prebuilt/tool_node.py
index cdcbdd819..1ea0dd56c 100644
--- a/libs/langgraph/langgraph/prebuilt/tool_node.py
+++ b/libs/langgraph/langgraph/prebuilt/tool_node.py
@@ -37,7 +37,7 @@ from langchain_core.tools import tool as create_tool
from langchain_core.tools.base import get_all_basemodel_annotations
from typing_extensions import Annotated, get_args, get_origin
-from langgraph.errors import GraphInterrupt
+from langgraph.errors import GraphBubbleUp
from langgraph.store.base import BaseStore
from langgraph.utils.runnable import RunnableCallable
@@ -275,7 +275,7 @@ class ToolNode(RunnableCallable):
# (2) a NodeInterrupt is raised inside a graph node for a graph called as a tool
# (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph called as a tool
# (2 and 3 can happen in a "supervisor w/ tools" multi-agent architecture)
- except GraphInterrupt as e:
+ except GraphBubbleUp as e:
raise e
except Exception as e:
if isinstance(self.handle_tool_errors, tuple):
@@ -316,7 +316,7 @@ class ToolNode(RunnableCallable):
# (2) a NodeInterrupt is raised inside a graph node for a graph called as a tool
# (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph called as a tool
# (2 and 3 can happen in a "supervisor w/ tools" multi-agent architecture)
- except GraphInterrupt as e:
+ except GraphBubbleUp as e:
raise e
except Exception as e:
if isinstance(self.handle_tool_errors, tuple):
diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py
index 564c53022..1410e432f 100644
--- a/libs/langgraph/langgraph/pregel/algo.py
+++ b/libs/langgraph/langgraph/pregel/algo.py
@@ -602,6 +602,7 @@ def prepare_single_task(
None,
task_id,
task_path,
+ writers=proc.flat_writers,
)
else:
@@ -720,6 +721,7 @@ def prepare_single_task(
None,
task_id,
task_path,
+ writers=proc.flat_writers,
)
else:
return PregelTask(task_id, name, task_path)
diff --git a/libs/langgraph/langgraph/pregel/executor.py b/libs/langgraph/langgraph/pregel/executor.py
index 246510fb4..70aea29e3 100644
--- a/libs/langgraph/langgraph/pregel/executor.py
+++ b/libs/langgraph/langgraph/pregel/executor.py
@@ -20,7 +20,7 @@ from langchain_core.runnables import RunnableConfig
from langchain_core.runnables.config import get_executor_for_config
from typing_extensions import ParamSpec
-from langgraph.errors import GraphInterrupt
+from langgraph.errors import GraphBubbleUp
P = ParamSpec("P")
T = TypeVar("T")
@@ -68,7 +68,7 @@ class BackgroundExecutor(ContextManager):
def done(self, task: concurrent.futures.Future) -> None:
try:
task.result()
- except GraphInterrupt:
+ except GraphBubbleUp:
# This exception is an interruption signal, not an error
# so we don't want to re-raise it on exit
self.tasks.pop(task)
@@ -155,7 +155,7 @@ class AsyncBackgroundExecutor(AsyncContextManager):
if exc := task.exception():
# This exception is an interruption signal, not an error
# so we don't want to re-raise it on exit
- if isinstance(exc, GraphInterrupt):
+ if isinstance(exc, GraphBubbleUp):
self.tasks.pop(task)
else:
self.tasks.pop(task)
diff --git a/libs/langgraph/langgraph/pregel/io.py b/libs/langgraph/langgraph/pregel/io.py
index 6695e1ce0..693dffce2 100644
--- a/libs/langgraph/langgraph/pregel/io.py
+++ b/libs/langgraph/langgraph/pregel/io.py
@@ -15,6 +15,7 @@ from langgraph.constants import (
TAG_HIDDEN,
TASKS,
)
+from langgraph.errors import InvalidUpdateError
from langgraph.pregel.log import logger
from langgraph.types import Command, PregelExecutableTask, Send
@@ -68,6 +69,8 @@ def map_command(
cmd: Command,
) -> Iterator[tuple[str, str, Any]]:
"""Map input chunk to a sequence of pending writes in the form (channel, value)."""
+ if cmd.graph == Command.PARENT:
+ raise InvalidUpdateError("There is not parent graph")
if cmd.send:
if isinstance(cmd.send, (tuple, list)):
sends = cmd.send
diff --git a/libs/langgraph/langgraph/pregel/retry.py b/libs/langgraph/langgraph/pregel/retry.py
index ea9162dc2..6e52a7c41 100644
--- a/libs/langgraph/langgraph/pregel/retry.py
+++ b/libs/langgraph/langgraph/pregel/retry.py
@@ -2,6 +2,7 @@ import asyncio
import logging
import random
import time
+from dataclasses import replace
from functools import partial
from typing import Any, Callable, Optional, Sequence
@@ -10,9 +11,10 @@ from langgraph.constants import (
CONFIG_KEY_CHECKPOINT_NS,
CONFIG_KEY_RESUMING,
CONFIG_KEY_SEND,
+ NS_SEP,
)
-from langgraph.errors import _SEEN_CHECKPOINT_NS, GraphInterrupt
-from langgraph.types import PregelExecutableTask, RetryPolicy
+from langgraph.errors import _SEEN_CHECKPOINT_NS, GraphBubbleUp, ParentCommand
+from langgraph.types import Command, PregelExecutableTask, RetryPolicy
from langgraph.utils.config import patch_configurable
logger = logging.getLogger(__name__)
@@ -40,7 +42,21 @@ def run_with_retry(
task.proc.invoke(task.input, config)
# if successful, end
break
- except GraphInterrupt:
+ except ParentCommand as exc:
+ ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]
+ cmd = exc.args[0]
+ if cmd.graph == ns:
+ # this command is for the current graph, handle it
+ for w in task.writers:
+ w.invoke(cmd, config)
+ break
+ elif cmd.graph == Command.PARENT:
+ # this command is for the parent graph, assign it to the parent
+ parent_ns = NS_SEP.join(ns.split(NS_SEP)[:-1])
+ exc.args = (replace(cmd, graph=parent_ns),)
+ # bubble up
+ raise
+ except GraphBubbleUp:
# if interrupted, end
raise
except Exception as exc:
@@ -118,7 +134,21 @@ async def arun_with_retry(
await task.proc.ainvoke(task.input, config)
# if successful, end
break
- except GraphInterrupt:
+ except ParentCommand as exc:
+ ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]
+ cmd = exc.args[0]
+ if cmd.graph == ns:
+ # this command is for the current graph, handle it
+ for w in task.writers:
+ w.invoke(cmd, config)
+ break
+ elif cmd.graph == Command.PARENT:
+ # this command is for the parent graph, assign it to the parent
+ parent_ns = NS_SEP.join(ns.split(NS_SEP)[:-1])
+ exc.args = (replace(cmd, graph=parent_ns),)
+ # bubble up
+ raise
+ except GraphBubbleUp:
# if interrupted, end
raise
except Exception as exc:
diff --git a/libs/langgraph/langgraph/pregel/runner.py b/libs/langgraph/langgraph/pregel/runner.py
index 64e5c8d3c..9e3879b0f 100644
--- a/libs/langgraph/langgraph/pregel/runner.py
+++ b/libs/langgraph/langgraph/pregel/runner.py
@@ -23,7 +23,7 @@ from langgraph.constants import (
PUSH,
TAG_HIDDEN,
)
-from langgraph.errors import GraphDelegate, GraphInterrupt
+from langgraph.errors import GraphBubbleUp, GraphInterrupt
from langgraph.pregel.executor import Submit
from langgraph.pregel.retry import arun_with_retry, run_with_retry
from langgraph.types import PregelExecutableTask, RetryPolicy
@@ -298,7 +298,7 @@ class PregelRunner:
# save interrupt to checkpointer
if interrupts := [(INTERRUPT, i) for i in exception.args[0]]:
self.put_writes(task.id, interrupts)
- elif isinstance(exception, GraphDelegate):
+ elif isinstance(exception, GraphBubbleUp):
raise exception
else:
# save error to checkpointer
@@ -324,7 +324,7 @@ def _should_stop_others(
if fut.cancelled():
return True
if exc := fut.exception():
- return not isinstance(exc, GraphInterrupt)
+ return not isinstance(exc, GraphBubbleUp)
else:
return False
diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py
index 104412d8e..0a1981647 100644
--- a/libs/langgraph/langgraph/types.py
+++ b/libs/langgraph/langgraph/types.py
@@ -5,6 +5,7 @@ from typing import (
TYPE_CHECKING,
Any,
Callable,
+ ClassVar,
Generic,
Hashable,
Literal,
@@ -140,6 +141,7 @@ class PregelExecutableTask(NamedTuple):
id: str
path: tuple[Union[str, int, tuple], ...]
scheduled: bool = False
+ writers: Sequence[Runnable] = ()
class StateSnapshot(NamedTuple):
@@ -233,12 +235,14 @@ class Send:
N = TypeVar("N", bound=Hashable)
+PARENT = Literal["__parent__"]
@dataclasses.dataclass(**_DC_KWARGS)
class Command(Generic[N]):
"""One or more commands to update the graph's state and send messages to nodes."""
+ graph: Optional[Union[PARENT, str]] = None
update: Optional[dict[str, Any]] = None
send: Union[Send, Sequence[Send]] = ()
resume: Optional[Union[Any, dict[str, Any]]] = None
@@ -252,6 +256,8 @@ class Command(Generic[N]):
)
return f"Command({contents})"
+ PARENT = ClassVar[PARENT] = "__parent__"
+
StreamChunk = tuple[tuple[str, ...], str, Any]
From 5bbb9dae575a650fc1b027c972ac9fa845cdb698 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Fri, 22 Nov 2024 16:34:55 -0800
Subject: [PATCH 040/149] Fix
---
libs/langgraph/langgraph/types.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py
index 0a1981647..323130b52 100644
--- a/libs/langgraph/langgraph/types.py
+++ b/libs/langgraph/langgraph/types.py
@@ -256,7 +256,7 @@ class Command(Generic[N]):
)
return f"Command({contents})"
- PARENT = ClassVar[PARENT] = "__parent__"
+ PARENT: ClassVar[Literal["__parent__"]] = "__parent__"
StreamChunk = tuple[tuple[str, ...], str, Any]
From abc0c8c2235278a164f0ccd000addf1e401f6351 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Fri, 22 Nov 2024 16:35:20 -0800
Subject: [PATCH 041/149] Fix
---
libs/langgraph/langgraph/types.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py
index 323130b52..7bf9148c5 100644
--- a/libs/langgraph/langgraph/types.py
+++ b/libs/langgraph/langgraph/types.py
@@ -235,14 +235,13 @@ class Send:
N = TypeVar("N", bound=Hashable)
-PARENT = Literal["__parent__"]
@dataclasses.dataclass(**_DC_KWARGS)
class Command(Generic[N]):
"""One or more commands to update the graph's state and send messages to nodes."""
- graph: Optional[Union[PARENT, str]] = None
+ graph: Optional[str] = None
update: Optional[dict[str, Any]] = None
send: Union[Send, Sequence[Send]] = ()
resume: Optional[Union[Any, dict[str, Any]]] = None
From 486d5412af683c509674f4c3086cb2102c87aa5e Mon Sep 17 00:00:00 2001
From: Talha Munir <103119362+devs-talha@users.noreply.github.com>
Date: Sun, 24 Nov 2024 00:37:55 +0500
Subject: [PATCH 042/149] docs: Fix grammatical mistake in introduction.ipynb
(#2521)
---
docs/docs/tutorials/introduction.ipynb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/docs/tutorials/introduction.ipynb b/docs/docs/tutorials/introduction.ipynb
index 712fbc28f..119fbf861 100644
--- a/docs/docs/tutorials/introduction.ipynb
+++ b/docs/docs/tutorials/introduction.ipynb
@@ -2043,7 +2043,7 @@
"\n",
"So far, we've relied on a simple state (it's just a list of messages!). You can go far with this simple state, but if you want to define complex behavior without relying on the message list, you can add additional fields to the state. In this section, we will extend our chat bot with a new node to illustrate this.\n",
"\n",
- "In the examples above, we involved a human deterministically: the graph __always__ interrupted whenever an tool was invoked. Suppose we wanted our chat bot to have the choice of relying on a human.\n",
+ "In the examples above, we involved a human deterministically: the graph __always__ interrupted whenever a tool was invoked. Suppose we wanted our chat bot to have the choice of relying on a human.\n",
"\n",
"One way to do this is to create a passthrough \"human\" node, before which the graph will always stop. We will only execute this node if the LLM invokes a \"human\" tool. For our convenience, we will include an \"ask_human\" flag in our graph state that we will flip if the LLM calls this tool.\n",
"\n",
From 328ef609afc74a91ecb52a05ac06447125c12a91 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Mon, 25 Nov 2024 11:59:49 -0800
Subject: [PATCH 043/149] [CLI] Python path (#2531)
---
libs/cli/langgraph_cli/cli.py | 8 ++++++++
libs/cli/pyproject.toml | 2 +-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py
index 07c894fac..c304252b3 100644
--- a/libs/cli/langgraph_cli/cli.py
+++ b/libs/cli/langgraph_cli/cli.py
@@ -1,3 +1,4 @@
+import os
import pathlib
import shutil
import sys
@@ -598,6 +599,13 @@ def dev(
) from None
config_json = langgraph_cli.config.validate_config_file(config)
+ cwd = os.getcwd()
+ sys.path.append(cwd)
+ dependencies = config_json.get("dependencies", [])
+ for dep in dependencies:
+ dep_path = pathlib.Path(cwd) / dep
+ if dep_path.is_dir() and dep_path.exists():
+ sys.path.append(str(dep_path))
graphs = config_json.get("graphs", {})
run_server(
diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml
index a271a073b..163bb4a31 100644
--- a/libs/cli/pyproject.toml
+++ b/libs/cli/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
-version = "0.1.58"
+version = "0.1.59"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
From 98935e1ffd8ab82d5587b90fe154ecf92fa61121 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Mon, 25 Nov 2024 12:19:52 -0800
Subject: [PATCH 044/149] fix: Fix race condition in PostgresSaver (#2494)
Signed-off-by: Tyler Ball
Co-authored-by: Phoenix Logan
Co-authored-by: Tyler Ball <2481463+tyler-ball@users.noreply.github.com>
---
.../langgraph/checkpoint/postgres/__init__.py | 26 +-
.../checkpoint/postgres/_ainternal.py | 23 +
.../checkpoint/postgres/_internal.py | 21 +
.../langgraph/checkpoint/postgres/aio.py | 65 ++-
.../langgraph/store/postgres/aio.py | 254 +++++----
.../langgraph/store/postgres/base.py | 217 +++++---
libs/checkpoint-postgres/tests/conftest.py | 6 +-
.../tests/test_async_store.py | 223 +++-----
libs/checkpoint-postgres/tests/test_store.py | 501 +++++++-----------
libs/langgraph/Makefile | 7 +-
libs/langgraph/tests/conftest.py | 109 +++-
libs/langgraph/tests/test_pregel.py | 115 ++--
libs/langgraph/tests/test_pregel_async.py | 90 +++-
13 files changed, 906 insertions(+), 751 deletions(-)
create mode 100644 libs/checkpoint-postgres/langgraph/checkpoint/postgres/_ainternal.py
create mode 100644 libs/checkpoint-postgres/langgraph/checkpoint/postgres/_internal.py
diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py
index b8138a945..2107b05a3 100644
--- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py
+++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py
@@ -1,6 +1,6 @@
import threading
from contextlib import contextmanager
-from typing import Any, Iterator, Optional, Sequence, Union
+from typing import Any, Iterator, Optional, Sequence
from langchain_core.runnables import RunnableConfig
from psycopg import Capabilities, Connection, Cursor, Pipeline
@@ -17,21 +17,11 @@ from langgraph.checkpoint.base import (
CheckpointTuple,
get_checkpoint_id,
)
+from langgraph.checkpoint.postgres import _internal
from langgraph.checkpoint.postgres.base import BasePostgresSaver
from langgraph.checkpoint.serde.base import SerializerProtocol
-Conn = Union[Connection[DictRow], ConnectionPool[Connection[DictRow]]]
-
-
-@contextmanager
-def _get_connection(conn: Conn) -> Iterator[Connection[DictRow]]:
- if isinstance(conn, Connection):
- yield conn
- elif isinstance(conn, ConnectionPool):
- with conn.connection() as conn:
- yield conn
- else:
- raise TypeError(f"Invalid connection type: {type(conn)}")
+Conn = _internal.Conn # For backward compatibility
class PostgresSaver(BasePostgresSaver):
@@ -39,7 +29,7 @@ class PostgresSaver(BasePostgresSaver):
def __init__(
self,
- conn: Conn,
+ conn: _internal.Conn,
pipe: Optional[Pipeline] = None,
serde: Optional[SerializerProtocol] = None,
) -> None:
@@ -73,9 +63,9 @@ class PostgresSaver(BasePostgresSaver):
) as conn:
if pipeline:
with conn.pipeline() as pipe:
- yield PostgresSaver(conn, pipe)
+ yield cls(conn, pipe)
else:
- yield PostgresSaver(conn)
+ yield cls(conn)
def setup(self) -> None:
"""Set up the checkpoint database asynchronously.
@@ -373,7 +363,7 @@ class PostgresSaver(BasePostgresSaver):
Will be applied regardless of whether the PostgresSaver instance was initialized with a pipeline.
If pipeline mode is not supported, will fall back to using transaction context manager.
"""
- with _get_connection(self.conn) as conn:
+ with _internal.get_connection(self.conn) as conn:
if self.pipe:
# a connection in pipeline mode can be used concurrently
# in multiple threads/coroutines, but only one cursor can be
@@ -403,4 +393,4 @@ class PostgresSaver(BasePostgresSaver):
yield cur
-__all__ = ["PostgresSaver", "Conn"]
+__all__ = ["PostgresSaver", "BasePostgresSaver", "Conn"]
diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_ainternal.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_ainternal.py
new file mode 100644
index 000000000..a0b8b10f5
--- /dev/null
+++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_ainternal.py
@@ -0,0 +1,23 @@
+"""Shared async utility functions for the Postgres checkpoint & storage classes."""
+
+from contextlib import asynccontextmanager
+from typing import AsyncIterator, Union
+
+from psycopg import AsyncConnection
+from psycopg.rows import DictRow
+from psycopg_pool import AsyncConnectionPool
+
+Conn = Union[AsyncConnection[DictRow], AsyncConnectionPool[AsyncConnection[DictRow]]]
+
+
+@asynccontextmanager
+async def get_connection(
+ conn: Conn,
+) -> AsyncIterator[AsyncConnection[DictRow]]:
+ if isinstance(conn, AsyncConnection):
+ yield conn
+ elif isinstance(conn, AsyncConnectionPool):
+ async with conn.connection() as conn:
+ yield conn
+ else:
+ raise TypeError(f"Invalid connection type: {type(conn)}")
diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_internal.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_internal.py
new file mode 100644
index 000000000..b703262f2
--- /dev/null
+++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_internal.py
@@ -0,0 +1,21 @@
+"""Shared utility functions for the Postgres checkpoint & storage classes."""
+
+from contextlib import contextmanager
+from typing import Iterator, Union
+
+from psycopg import Connection
+from psycopg.rows import DictRow
+from psycopg_pool import ConnectionPool
+
+Conn = Union[Connection[DictRow], ConnectionPool[Connection[DictRow]]]
+
+
+@contextmanager
+def get_connection(conn: Conn) -> Iterator[Connection[DictRow]]:
+ if isinstance(conn, Connection):
+ yield conn
+ elif isinstance(conn, ConnectionPool):
+ with conn.connection() as conn:
+ yield conn
+ else:
+ raise TypeError(f"Invalid connection type: {type(conn)}")
diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
index 5b67e4ca9..589520efc 100644
--- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
+++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
@@ -1,6 +1,6 @@
import asyncio
from contextlib import asynccontextmanager
-from typing import Any, AsyncIterator, Iterator, Optional, Sequence, Union
+from typing import Any, AsyncIterator, Iterator, Optional, Sequence
from langchain_core.runnables import RunnableConfig
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
@@ -17,23 +17,11 @@ from langgraph.checkpoint.base import (
CheckpointTuple,
get_checkpoint_id,
)
+from langgraph.checkpoint.postgres import _ainternal
from langgraph.checkpoint.postgres.base import BasePostgresSaver
from langgraph.checkpoint.serde.base import SerializerProtocol
-Conn = Union[AsyncConnection[DictRow], AsyncConnectionPool[AsyncConnection[DictRow]]]
-
-
-@asynccontextmanager
-async def _get_connection(
- conn: Conn,
-) -> AsyncIterator[AsyncConnection[DictRow]]:
- if isinstance(conn, AsyncConnection):
- yield conn
- elif isinstance(conn, AsyncConnectionPool):
- async with conn.connection() as conn:
- yield conn
- else:
- raise TypeError(f"Invalid connection type: {type(conn)}")
+Conn = _ainternal.Conn # For backward compatibility
class AsyncPostgresSaver(BasePostgresSaver):
@@ -41,7 +29,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
def __init__(
self,
- conn: Conn,
+ conn: _ainternal.Conn,
pipe: Optional[AsyncPipeline] = None,
serde: Optional[SerializerProtocol] = None,
) -> None:
@@ -80,9 +68,9 @@ class AsyncPostgresSaver(BasePostgresSaver):
) as conn:
if pipeline:
async with conn.pipeline() as pipe:
- yield AsyncPostgresSaver(conn=conn, pipe=pipe, serde=serde)
+ yield cls(conn=conn, pipe=pipe, serde=serde)
else:
- yield AsyncPostgresSaver(conn=conn, serde=serde)
+ yield cls(conn=conn, serde=serde)
async def setup(self) -> None:
"""Set up the checkpoint database asynchronously.
@@ -157,15 +145,17 @@ class AsyncPostgresSaver(BasePostgresSaver):
value["pending_sends"],
),
self._load_metadata(value["metadata"]),
- {
- "configurable": {
- "thread_id": value["thread_id"],
- "checkpoint_ns": value["checkpoint_ns"],
- "checkpoint_id": value["parent_checkpoint_id"],
+ (
+ {
+ "configurable": {
+ "thread_id": value["thread_id"],
+ "checkpoint_ns": value["checkpoint_ns"],
+ "checkpoint_id": value["parent_checkpoint_id"],
+ }
}
- }
- if value["parent_checkpoint_id"]
- else None,
+ if value["parent_checkpoint_id"]
+ else None
+ ),
await asyncio.to_thread(self._load_writes, value["pending_writes"]),
)
@@ -216,15 +206,17 @@ class AsyncPostgresSaver(BasePostgresSaver):
value["pending_sends"],
),
self._load_metadata(value["metadata"]),
- {
- "configurable": {
- "thread_id": thread_id,
- "checkpoint_ns": checkpoint_ns,
- "checkpoint_id": value["parent_checkpoint_id"],
+ (
+ {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": value["parent_checkpoint_id"],
+ }
}
- }
- if value["parent_checkpoint_id"]
- else None,
+ if value["parent_checkpoint_id"]
+ else None
+ ),
await asyncio.to_thread(self._load_writes, value["pending_writes"]),
)
@@ -331,7 +323,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
Will be applied regardless of whether the AsyncPostgresSaver instance was initialized with a pipeline.
If pipeline mode is not supported, will fall back to using transaction context manager.
"""
- async with _get_connection(self.conn) as conn:
+ async with _ainternal.get_connection(self.conn) as conn:
if self.pipe:
# a connection in pipeline mode can be used concurrently
# in multiple threads/coroutines, but only one cursor can be
@@ -467,3 +459,6 @@ class AsyncPostgresSaver(BasePostgresSaver):
return asyncio.run_coroutine_threadsafe(
self.aput_writes(config, writes, task_id), self.loop
).result()
+
+
+__all__ = ["AsyncPostgresSaver", "Conn"]
diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/aio.py b/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
index dda7321d0..578523052 100644
--- a/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
+++ b/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
@@ -13,14 +13,17 @@ from typing import (
)
import orjson
-from psycopg import AsyncConnection, AsyncCursor
+from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
from psycopg.errors import UndefinedTable
-from psycopg.rows import dict_row
+from psycopg.rows import DictRow, dict_row
+from psycopg_pool import AsyncConnectionPool
+from langgraph.checkpoint.postgres import _ainternal
from langgraph.store.base import GetOp, ListNamespacesOp, Op, PutOp, Result, SearchOp
from langgraph.store.base.batch import AsyncBatchedBaseStore
from langgraph.store.postgres.base import (
BasePostgresStore,
+ PoolConfig,
Row,
_decode_ns_bytes,
_group_ops,
@@ -30,81 +33,88 @@ from langgraph.store.postgres.base import (
logger = logging.getLogger(__name__)
-class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[AsyncConnection]):
- __slots__ = ("_deserializer",)
+class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Conn]):
+ __slots__ = ("_deserializer", "pipe", "lock", "supports_pipeline")
def __init__(
self,
- conn: AsyncConnection[Any],
+ conn: _ainternal.Conn,
*,
+ pipe: Optional[AsyncPipeline] = None,
deserializer: Optional[
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
] = None,
) -> None:
+ if isinstance(conn, AsyncConnectionPool) and pipe is not None:
+ raise ValueError(
+ "Pipeline should be used only with a single AsyncConnection, not AsyncConnectionPool."
+ )
super().__init__()
self._deserializer = deserializer
self.conn = conn
+ self.pipe = pipe
+ self.lock = asyncio.Lock()
self.loop = asyncio.get_running_loop()
+ self.supports_pipeline = Capabilities().has_pipeline()
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
grouped_ops, num_ops = _group_ops(ops)
results: list[Result] = [None] * num_ops
- async with self.conn.pipeline():
- tasks = []
-
- if GetOp in grouped_ops:
- tasks.append(
- self._batch_get_ops(
- cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]), results
- )
- )
-
- if PutOp in grouped_ops:
- tasks.append(
- self._batch_put_ops(
- cast(Sequence[tuple[int, PutOp]], grouped_ops[PutOp])
- )
- )
-
- if SearchOp in grouped_ops:
- tasks.append(
- self._batch_search_ops(
- cast(Sequence[tuple[int, SearchOp]], grouped_ops[SearchOp]),
- results,
- )
- )
-
- if ListNamespacesOp in grouped_ops:
- tasks.append(
- self._batch_list_namespaces_ops(
- cast(
- Sequence[tuple[int, ListNamespacesOp]],
- grouped_ops[ListNamespacesOp],
- ),
- results,
- )
- )
-
- await asyncio.gather(*tasks)
+ async with _ainternal.get_connection(self.conn) as conn:
+ if self.pipe:
+ async with self.pipe:
+ await self._execute_batch(grouped_ops, results, conn)
+ else:
+ await self._execute_batch(grouped_ops, results, conn)
return results
- def batch(self, ops: Iterable[Op]) -> list[Result]:
- return asyncio.run_coroutine_threadsafe(self.abatch(ops), self.loop).result()
+ async def _execute_batch(
+ self,
+ grouped_ops: dict,
+ results: list[Result],
+ conn: AsyncConnection[DictRow],
+ ) -> None:
+ async with self._cursor(conn, pipeline=True) as cur:
+ if GetOp in grouped_ops:
+ await self._batch_get_ops(
+ cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]),
+ results,
+ cur,
+ )
+
+ if SearchOp in grouped_ops:
+ await self._batch_search_ops(
+ cast(Sequence[tuple[int, SearchOp]], grouped_ops[SearchOp]),
+ results,
+ cur,
+ )
+
+ if ListNamespacesOp in grouped_ops:
+ await self._batch_list_namespaces_ops(
+ cast(
+ Sequence[tuple[int, ListNamespacesOp]],
+ grouped_ops[ListNamespacesOp],
+ ),
+ results,
+ cur,
+ )
+
+ if PutOp in grouped_ops:
+ await self._batch_put_ops(
+ cast(Sequence[tuple[int, PutOp]], grouped_ops[PutOp]),
+ cur,
+ )
async def _batch_get_ops(
self,
get_ops: Sequence[tuple[int, GetOp]],
results: list[Result],
+ cur: AsyncCursor[DictRow],
) -> None:
- cursors = []
for query, params, namespace, items in self._get_batch_GET_ops_queries(get_ops):
- cur = self.conn.cursor(binary=True)
await cur.execute(query, params)
- cursors.append((cur, namespace, items))
-
- for cur, namespace, items in cursors:
rows = cast(list[Row], await cur.fetchall())
key_to_row = {row["key"]: row for row in rows}
for idx, key in items:
@@ -119,26 +129,21 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[AsyncConnectio
async def _batch_put_ops(
self,
put_ops: Sequence[tuple[int, PutOp]],
+ cur: AsyncCursor[DictRow],
) -> None:
queries = self._get_batch_PUT_queries(put_ops)
for query, params in queries:
- cur = self.conn.cursor(binary=True)
await cur.execute(query, params)
async def _batch_search_ops(
self,
search_ops: Sequence[tuple[int, SearchOp]],
results: list[Result],
+ cur: AsyncCursor[DictRow],
) -> None:
queries = self._get_batch_search_queries(search_ops)
- cursors: list[tuple[AsyncCursor[Any], int]] = []
-
for (query, params), (idx, _) in zip(queries, search_ops):
- cur = self.conn.cursor(binary=True)
await cur.execute(query, params)
- cursors.append((cur, idx))
-
- for cur, idx in cursors:
rows = cast(list[Row], await cur.fetchall())
items = [
_row_to_item(
@@ -152,37 +157,103 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[AsyncConnectio
self,
list_ops: Sequence[tuple[int, ListNamespacesOp]],
results: list[Result],
+ cur: AsyncCursor[DictRow],
) -> None:
queries = self._get_batch_list_namespaces_queries(list_ops)
- cursors: list[tuple[AsyncCursor[Any], int]] = []
for (query, params), (idx, _) in zip(queries, list_ops):
- cur = self.conn.cursor(binary=True)
await cur.execute(query, params)
- cursors.append((cur, idx))
-
- for cur, idx in cursors:
rows = cast(list[dict], await cur.fetchall())
namespaces = [_decode_ns_bytes(row["truncated_prefix"]) for row in rows]
results[idx] = namespaces
+ @asynccontextmanager
+ async def _cursor(
+ self, conn: AsyncConnection[DictRow], *, pipeline: bool = False
+ ) -> AsyncIterator[AsyncCursor[Any]]:
+ """Create a database cursor as a context manager.
+
+ Args:
+ conn: The database connection to use
+ pipeline: whether to use pipeline for the DB operations inside the context manager.
+ Will be applied regardless of whether the PostgresStore instance was initialized with a pipeline.
+ If pipeline mode is not supported, will fall back to using transaction context manager.
+ """
+ if self.pipe:
+ # a connection in pipeline mode can be used concurrently
+ # in multiple threads/coroutines, but only one cursor can be
+ # used at a time
+ async with conn.cursor(binary=True) as cur:
+ try:
+ yield cur
+ finally:
+ 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
+ if self.supports_pipeline:
+ async with self.lock, conn.pipeline(), conn.cursor(binary=True) as cur:
+ yield cur
+ else:
+ async with self.lock, conn.transaction(), conn.cursor(
+ binary=True
+ ) as cur:
+ yield cur
+ else:
+ async with conn.cursor(binary=True) as cur:
+ yield cur
+
+ def batch(self, ops: Iterable[Op]) -> list[Result]:
+ return asyncio.run_coroutine_threadsafe(self.abatch(ops), self.loop).result()
+
@classmethod
@asynccontextmanager
async def from_conn_string(
cls,
conn_string: str,
+ *,
+ pipeline: bool = False,
+ pool_config: Optional[PoolConfig] = None,
) -> AsyncIterator["AsyncPostgresStore"]:
"""Create a new AsyncPostgresStore instance from a connection string.
Args:
conn_string (str): The Postgres connection info string.
+ pipeline (bool): Whether to use AsyncPipeline (only for single connections)
+ pool_config (Optional[PoolConfig]): Configuration for the connection pool.
+ If provided, will create a connection pool and use it instead of a single connection.
+ This overrides the `pipeline` argument.
Returns:
AsyncPostgresStore: A new AsyncPostgresStore instance.
"""
- async with await AsyncConnection.connect(
- conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
- ) as conn:
- yield cls(conn=conn)
+ if pool_config is not None:
+ pc = pool_config.copy()
+ async with cast(
+ AsyncConnectionPool[AsyncConnection[DictRow]],
+ AsyncConnectionPool(
+ conn_string,
+ min_size=pc.pop("min_size", 1),
+ max_size=pc.pop("max_size", None),
+ kwargs={
+ "autocommit": True,
+ "prepare_threshold": 0,
+ "row_factory": dict_row,
+ **(pc.pop("kwargs", None) or {}),
+ },
+ **cast(dict, pc),
+ ),
+ ) as pool:
+ yield cls(conn=pool)
+ else:
+ async with await AsyncConnection.connect(
+ conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
+ ) as conn:
+ if pipeline:
+ async with conn.pipeline() as pipe:
+ yield cls(conn=conn, pipe=pipe)
+ else:
+ yield cls(conn=conn)
async def setup(self) -> None:
"""Set up the store database asynchronously.
@@ -191,28 +262,33 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[AsyncConnectio
already exist and runs database migrations. It MUST be called directly by the user
the first time the store is used.
"""
- async with self.conn.cursor() as cur:
- try:
- await cur.execute(
- "SELECT v FROM store_migrations ORDER BY v DESC LIMIT 1"
- )
- row = cast(dict, await cur.fetchone())
- if row is None:
- version = -1
- else:
- version = row["v"]
- except UndefinedTable:
- version = -1
- # Create store_migrations table if it doesn't exist
- await cur.execute(
- """
- CREATE TABLE IF NOT EXISTS store_migrations (
- v INTEGER PRIMARY KEY
+ async with _ainternal.get_connection(self.conn) as conn:
+ async with conn.cursor() as cur:
+ try:
+ await cur.execute(
+ "SELECT v FROM store_migrations ORDER BY v DESC LIMIT 1"
)
- """
- )
- for v, migration in enumerate(
- self.MIGRATIONS[version + 1 :], start=version + 1
- ):
- await cur.execute(migration)
- await cur.execute("INSERT INTO store_migrations (v) VALUES (%s)", (v,))
+ row = cast(dict, await cur.fetchone())
+ if row is None:
+ version = -1
+ else:
+ version = row["v"]
+ except UndefinedTable:
+ version = -1
+ # Create store_migrations table if it doesn't exist
+ await cur.execute(
+ """
+ CREATE TABLE IF NOT EXISTS store_migrations (
+ v INTEGER PRIMARY KEY
+ )
+ """
+ )
+ for v, migration in enumerate(
+ self.MIGRATIONS[version + 1 :], start=version + 1
+ ):
+ await cur.execute(migration)
+ await cur.execute(
+ "INSERT INTO store_migrations (v) VALUES (%s)", (v,)
+ )
+ if self.pipe:
+ await self.pipe.sync()
diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/base.py b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
index 8bd2b8279..3bb343b0c 100644
--- a/libs/checkpoint-postgres/langgraph/store/postgres/base.py
+++ b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
@@ -1,6 +1,7 @@
import asyncio
import json
import logging
+import threading
from collections import defaultdict
from contextlib import contextmanager
from datetime import datetime
@@ -18,12 +19,15 @@ from typing import (
)
import orjson
-from psycopg import BaseConnection, Connection, Cursor
+from psycopg import Capabilities, Connection, Cursor, Pipeline
from psycopg.errors import UndefinedTable
-from psycopg.rows import dict_row
+from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
+from psycopg_pool import ConnectionPool
from typing_extensions import TypedDict
+from langgraph.checkpoint.postgres import _ainternal as _ainternal
+from langgraph.checkpoint.postgres import _internal as _pg_internal
from langgraph.store.base import (
BaseStore,
GetOp,
@@ -56,7 +60,32 @@ CREATE INDEX IF NOT EXISTS store_prefix_idx ON store USING btree (prefix text_pa
""",
]
-C = TypeVar("C", bound=BaseConnection)
+C = TypeVar("C", bound=Union[_pg_internal.Conn, _ainternal.Conn])
+
+
+class PoolConfig(TypedDict, total=False):
+ """Connection pool settings for PostgreSQL connections.
+
+ Controls connection lifecycle and resource utilization:
+ - Small pools (1-5) suit low-concurrency workloads
+ - Larger pools handle concurrent requests but consume more resources
+ - Setting max_size prevents resource exhaustion under load
+ """
+
+ min_size: int
+ """Minimum number of connections maintained in the pool. Defaults to 1."""
+
+ max_size: Optional[int]
+ """Maximum number of connections allowed in the pool. None means unlimited."""
+
+ kwargs: dict
+ """Additional connection arguments passed to each connection in the pool.
+
+ Default kwargs set automatically:
+ - autocommit: True
+ - prepare_threshold: 0
+ - row_factory: dict_row
+ """
class BasePostgresStore(Generic[C]):
@@ -88,9 +117,14 @@ class BasePostgresStore(Generic[C]):
self,
put_ops: Sequence[tuple[int, PutOp]],
) -> list[tuple[str, Sequence]]:
+ # Last-write wins
+ dedupped_ops: dict[tuple[tuple[str, ...], str], PutOp] = {}
+ for _, op in put_ops:
+ dedupped_ops[(op.namespace, op.key)] = op
+
inserts: list[PutOp] = []
deletes: list[PutOp] = []
- for _, op in put_ops:
+ for op in dedupped_ops.values():
if op.value is None:
deletes.append(op)
else:
@@ -219,13 +253,14 @@ class BasePostgresStore(Generic[C]):
return queries
-class PostgresStore(BaseStore, BasePostgresStore[Connection]):
- __slots__ = ("_deserializer",)
+class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
+ __slots__ = ("_deserializer", "pipe", "lock", "supports_pipeline")
def __init__(
self,
- conn: Connection[Any],
+ conn: _pg_internal.Conn,
*,
+ pipe: Optional[Pipeline] = None,
deserializer: Optional[
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
] = None,
@@ -233,26 +268,110 @@ class PostgresStore(BaseStore, BasePostgresStore[Connection]):
super().__init__()
self._deserializer = deserializer
self.conn = conn
+ self.pipe = pipe
+ self.supports_pipeline = Capabilities().has_pipeline()
+ self.lock = threading.Lock()
+
+ @classmethod
+ @contextmanager
+ def from_conn_string(
+ cls,
+ conn_string: str,
+ *,
+ pipeline: bool = False,
+ pool_config: Optional[PoolConfig] = None,
+ ) -> Iterator["PostgresStore"]:
+ """Create a new PostgresStore instance from a connection string.
+
+ Args:
+ conn_string (str): The Postgres connection info string.
+ pipeline (bool): whether to use Pipeline (only for single connections)
+ pool_config (Optional[PoolArgs]): Configuration for the connection pool.
+ If provided, will create a connection pool and use it instead of a single connection.
+ This overrides the `pipeline` argument.
+ Returns:
+ PostgresStore: A new PostgresStore instance.
+ """
+ if pool_config is not None:
+ pc = pool_config.copy()
+ with cast(
+ ConnectionPool[Connection[DictRow]],
+ ConnectionPool(
+ conn_string,
+ min_size=pc.pop("min_size", 1),
+ max_size=pc.pop("max_size", None),
+ kwargs={
+ "autocommit": True,
+ "prepare_threshold": 0,
+ "row_factory": dict_row,
+ **(pc.pop("kwargs", None) or {}),
+ },
+ **cast(dict, pc),
+ ),
+ ) as pool:
+ yield cls(conn=pool)
+ else:
+ with Connection.connect(
+ conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
+ ) as conn:
+ if pipeline:
+ with conn.pipeline() as pipe:
+ yield cls(conn, pipe=pipe)
+ else:
+ yield cls(conn)
+
+ @contextmanager
+ def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor[DictRow]]:
+ """Create a database cursor as a context manager.
+
+ Args:
+ pipeline (bool): whether to use pipeline for the DB operations inside the context manager.
+ Will be applied regardless of whether the PostgresStore instance was initialized with a pipeline.
+ If pipeline mode is not supported, will fall back to using transaction context manager.
+ """
+ with _pg_internal.get_connection(self.conn) as conn:
+ if self.pipe:
+ # a connection in pipeline mode can be used concurrently
+ # in multiple threads/coroutines, but only one cursor can be
+ # used at a time
+ try:
+ with conn.cursor(binary=True, row_factory=dict_row) as cur:
+ yield cur
+ finally:
+ 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
+ if self.supports_pipeline:
+ with self.lock, conn.pipeline(), conn.cursor(
+ binary=True, row_factory=dict_row
+ ) as cur:
+ yield cur
+ else:
+ with self.lock, conn.transaction(), conn.cursor(
+ binary=True, row_factory=dict_row
+ ) as cur:
+ yield cur
+ else:
+ with conn.cursor(binary=True, row_factory=dict_row) as cur:
+ yield cur
def batch(self, ops: Iterable[Op]) -> list[Result]:
grouped_ops, num_ops = _group_ops(ops)
results: list[Result] = [None] * num_ops
- with self.conn.pipeline():
+ with self._cursor(pipeline=True) as cur:
if GetOp in grouped_ops:
self._batch_get_ops(
- cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]), results
- )
-
- if PutOp in grouped_ops:
- self._batch_put_ops(
- cast(Sequence[tuple[int, PutOp]], grouped_ops[PutOp])
+ cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]), results, cur
)
if SearchOp in grouped_ops:
self._batch_search_ops(
cast(Sequence[tuple[int, SearchOp]], grouped_ops[SearchOp]),
results,
+ cur,
)
if ListNamespacesOp in grouped_ops:
@@ -262,25 +381,23 @@ class PostgresStore(BaseStore, BasePostgresStore[Connection]):
grouped_ops[ListNamespacesOp],
),
results,
+ cur,
+ )
+ if PutOp in grouped_ops:
+ self._batch_put_ops(
+ cast(Sequence[tuple[int, PutOp]], grouped_ops[PutOp]), cur
)
return results
- async def abatch(self, ops: Iterable[Op]) -> list[Result]:
- return await asyncio.get_running_loop().run_in_executor(None, self.batch, ops)
-
def _batch_get_ops(
self,
get_ops: Sequence[tuple[int, GetOp]],
results: list[Result],
+ cur: Cursor[DictRow],
) -> None:
- cursors = []
for query, params, namespace, items in self._get_batch_GET_ops_queries(get_ops):
- cur = self.conn.cursor(binary=True)
cur.execute(query, params)
- cursors.append((cur, namespace, items))
-
- for cur, namespace, items in cursors:
rows = cast(list[Row], cur.fetchall())
key_to_row = {row["key"]: row for row in rows}
for idx, key in items:
@@ -295,70 +412,44 @@ class PostgresStore(BaseStore, BasePostgresStore[Connection]):
def _batch_put_ops(
self,
put_ops: Sequence[tuple[int, PutOp]],
+ cur: Cursor[DictRow],
) -> None:
queries = self._get_batch_PUT_queries(put_ops)
for query, params in queries:
- cur = self.conn.cursor(binary=True)
cur.execute(query, params)
def _batch_search_ops(
self,
search_ops: Sequence[tuple[int, SearchOp]],
results: list[Result],
+ cur: Cursor[DictRow],
) -> None:
- queries = self._get_batch_search_queries(search_ops)
- cursors: list[tuple[Cursor[Any], int]] = []
-
- for (query, params), (idx, _) in zip(queries, search_ops):
- cur = self.conn.cursor(binary=True)
+ for (query, params), (idx, _) in zip(
+ self._get_batch_search_queries(search_ops), search_ops
+ ):
cur.execute(query, params)
- cursors.append((cur, idx))
-
- for cur, idx in cursors:
rows = cast(list[Row], cur.fetchall())
- items = [
+ results[idx] = [
_row_to_item(
_decode_ns_bytes(row["prefix"]), row, loader=self._deserializer
)
for row in rows
]
- results[idx] = items
def _batch_list_namespaces_ops(
self,
list_ops: Sequence[tuple[int, ListNamespacesOp]],
results: list[Result],
+ cur: Cursor[DictRow],
) -> None:
- queries = self._get_batch_list_namespaces_queries(list_ops)
- cursors: list[tuple[Cursor[Any], int]] = []
- for (query, params), (idx, _) in zip(queries, list_ops):
- cur = self.conn.cursor(binary=True)
+ for (query, params), (idx, _) in zip(
+ self._get_batch_list_namespaces_queries(list_ops), list_ops
+ ):
cur.execute(query, params)
- cursors.append((cur, idx))
+ results[idx] = [_decode_ns_bytes(row["truncated_prefix"]) for row in cur]
- for cur, idx in cursors:
- rows = cast(list[dict], cur.fetchall())
- namespaces = [_decode_ns_bytes(row["truncated_prefix"]) for row in rows]
- results[idx] = namespaces
-
- @classmethod
- @contextmanager
- def from_conn_string(
- cls,
- conn_string: str,
- ) -> Iterator["PostgresStore"]:
- """Create a new BasePostgresStore instance from a connection string.
-
- Args:
- conn_string (str): The Postgres connection info string.
-
- Returns:
- BasePostgresStore: A new BasePostgresStore instance.
- """
- with Connection.connect(
- conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
- ) as conn:
- yield cls(conn=conn)
+ async def abatch(self, ops: Iterable[Op]) -> list[Result]:
+ return await asyncio.get_running_loop().run_in_executor(None, self.batch, ops)
def setup(self) -> None:
"""Set up the store database.
@@ -367,7 +458,7 @@ class PostgresStore(BaseStore, BasePostgresStore[Connection]):
already exist and runs database migrations. It MUST be called directly by the user
the first time the store is used.
"""
- with self.conn.cursor(binary=True) as cur:
+ with self._cursor() as cur:
try:
cur.execute("SELECT v FROM store_migrations ORDER BY v DESC LIMIT 1")
row = cast(dict, cur.fetchone())
@@ -376,9 +467,7 @@ class PostgresStore(BaseStore, BasePostgresStore[Connection]):
else:
version = row["v"]
except UndefinedTable:
- self.conn.rollback()
version = -1
- # Create store_migrations table if it doesn't exist
cur.execute(
"""
CREATE TABLE IF NOT EXISTS store_migrations (
diff --git a/libs/checkpoint-postgres/tests/conftest.py b/libs/checkpoint-postgres/tests/conftest.py
index 49061b98c..56d199812 100644
--- a/libs/checkpoint-postgres/tests/conftest.py
+++ b/libs/checkpoint-postgres/tests/conftest.py
@@ -24,6 +24,10 @@ async def clear_test_db(conn: AsyncConnection[DictRow]) -> None:
await conn.execute("DELETE FROM checkpoint_blobs")
await conn.execute("DELETE FROM checkpoint_writes")
await conn.execute("DELETE FROM checkpoint_migrations")
- await conn.execute("DELETE FROM store_migrations")
+ except UndefinedTable:
+ pass
+ try:
+ await conn.execute("DELETE FROM store_migrations")
+ await conn.execute("DELETE FROM store")
except UndefinedTable:
pass
diff --git a/libs/checkpoint-postgres/tests/test_async_store.py b/libs/checkpoint-postgres/tests/test_async_store.py
index e7a7b31a1..71aaa4e36 100644
--- a/libs/checkpoint-postgres/tests/test_async_store.py
+++ b/libs/checkpoint-postgres/tests/test_async_store.py
@@ -1,114 +1,76 @@
# type: ignore
+import sys
import uuid
-from datetime import datetime
-from typing import Any
-from unittest.mock import AsyncMock, MagicMock
+from typing import AsyncIterator
import pytest
from conftest import DEFAULT_URI # type: ignore
+from psycopg import AsyncConnection
from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
from langgraph.store.postgres import AsyncPostgresStore
-class MockAsyncCursor:
- def __init__(self, fetch_result: Any) -> None:
- self.fetch_result = fetch_result
- self.execute = AsyncMock()
- self.fetchall = AsyncMock(return_value=self.fetch_result)
+@pytest.fixture(scope="function", params=["default", "pipe", "pool"])
+async def store(request) -> AsyncIterator[AsyncPostgresStore]:
+ if sys.version_info < (3, 10):
+ pytest.skip("Async Postgres tests require Python 3.10+")
+ database = f"test_{uuid.uuid4().hex[:16]}"
+ uri_parts = DEFAULT_URI.split("/")
+ uri_base = "/".join(uri_parts[:-1])
+ query_params = ""
+ if "?" in uri_parts[-1]:
+ db_name, query_params = uri_parts[-1].split("?", 1)
+ query_params = "?" + query_params
-class MockAsyncConnection:
- def __init__(self) -> None:
- self.cursor = MagicMock()
- self.pipeline = MagicMock(
- return_value=AsyncMock(__aenter__=AsyncMock(), __aexit__=AsyncMock())
- )
+ conn_string = f"{uri_base}/{database}{query_params}"
+ admin_conn_string = DEFAULT_URI
+ async with await AsyncConnection.connect(
+ admin_conn_string, autocommit=True
+ ) as conn:
+ await conn.execute(f"CREATE DATABASE {database}")
+ try:
+ async with AsyncPostgresStore.from_conn_string(conn_string) as store:
+ await store.setup()
-@pytest.fixture
-def mock_connection() -> MockAsyncConnection:
- return MockAsyncConnection()
-
-
-@pytest.fixture
-async def store(mock_connection: MockAsyncConnection) -> AsyncPostgresStore:
- return AsyncPostgresStore(mock_connection)
+ if request.param == "pipe":
+ async with AsyncPostgresStore.from_conn_string(
+ conn_string, pipeline=True
+ ) as store:
+ yield store
+ elif request.param == "pool":
+ async with AsyncPostgresStore.from_conn_string(
+ conn_string, pool_config={"min_size": 1, "max_size": 10}
+ ) as store:
+ yield store
+ else: # default
+ async with AsyncPostgresStore.from_conn_string(conn_string) as store:
+ yield store
+ finally:
+ async with await AsyncConnection.connect(
+ admin_conn_string, autocommit=True
+ ) as conn:
+ await conn.execute(f"DROP DATABASE {database}")
async def test_abatch_order(store: AsyncPostgresStore) -> None:
- mock_connection = store.conn
- mock_get_cursor = MockAsyncCursor(
- [
- {
- "key": "key1",
- "value": '{"data": "value1"}',
- "created_at": datetime.now(),
- "updated_at": datetime.now(),
- "prefix": "test.foo",
- },
- {
- "key": "key2",
- "value": '{"data": "value2"}',
- "created_at": datetime.now(),
- "updated_at": datetime.now(),
- "prefix": "test.bar",
- },
- ]
- )
- mock_search_cursor = MockAsyncCursor(
- [
- {
- "key": "key1",
- "value": '{"data": "value1"}',
- "created_at": datetime.now(),
- "updated_at": datetime.now(),
- "prefix": "test.foo",
- },
- ]
- )
- mock_list_namespaces_cursor = MockAsyncCursor(
- [
- {"truncated_prefix": b"\x01test"},
- ]
- )
-
- failures = []
-
- def cursor_side_effect(binary: bool = False) -> Any:
- cursor = MagicMock()
-
- async def execute_side_effect(query: str, *params: Any) -> None:
- # My super sophisticated database.
- if "SELECT prefix, key," in query:
- cursor.fetchall = mock_search_cursor.fetchall
- elif "SELECT DISTINCT ON (truncated_prefix)" in query:
- cursor.fetchall = mock_list_namespaces_cursor.fetchall
- elif "WHERE prefix = %s AND key" in query:
- cursor.fetchall = mock_get_cursor.fetchall
- elif "INSERT INTO " in query:
- pass
- else:
- e = ValueError(f"Unmatched query: {query}")
- failures.append(e)
- raise e
-
- cursor.execute = AsyncMock(side_effect=execute_side_effect)
- return cursor
-
- mock_connection.cursor.side_effect = cursor_side_effect # type: ignore
+ # Setup test data
+ await store.aput(("test", "foo"), "key1", {"data": "value1"})
+ await store.aput(("test", "bar"), "key2", {"data": "value2"})
ops = [
- GetOp(namespace=("test",), key="key1"),
- PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
+ GetOp(namespace=("test", "foo"), key="key1"),
+ PutOp(namespace=("test", "bar"), key="key2", value={"data": "value2"}),
SearchOp(
namespace_prefix=("test",), filter={"data": "value1"}, limit=10, offset=0
),
ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0),
GetOp(namespace=("test",), key="key3"),
]
+
results = await store.abatch(ops)
- assert not failures
assert len(results) == 5
assert isinstance(results[0], Item)
assert isinstance(results[0].value, dict)
@@ -118,27 +80,29 @@ async def test_abatch_order(store: AsyncPostgresStore) -> None:
assert isinstance(results[2], list)
assert len(results[2]) == 1
assert isinstance(results[3], list)
- assert results[3] == [("test",)]
+ assert ("test", "foo") in results[3] and ("test", "bar") in results[3]
assert results[4] is None
ops_reordered = [
SearchOp(namespace_prefix=("test",), filter=None, limit=5, offset=0),
- GetOp(namespace=("test",), key="key2"),
+ GetOp(namespace=("test", "bar"), key="key2"),
ListNamespacesOp(match_conditions=None, max_depth=None, limit=5, offset=0),
PutOp(namespace=("test",), key="key3", value={"data": "value3"}),
- GetOp(namespace=("test",), key="key1"),
+ GetOp(namespace=("test", "foo"), key="key1"),
]
results_reordered = await store.abatch(ops_reordered)
- assert not failures
assert len(results_reordered) == 5
assert isinstance(results_reordered[0], list)
- assert len(results_reordered[0]) == 1
+ assert len(results_reordered[0]) == 2
assert isinstance(results_reordered[1], Item)
assert results_reordered[1].value == {"data": "value2"}
assert results_reordered[1].key == "key2"
assert isinstance(results_reordered[2], list)
- assert results_reordered[2] == [("test",)]
+ assert ("test", "foo") in results_reordered[2] and (
+ "test",
+ "bar",
+ ) in results_reordered[2]
assert results_reordered[3] is None
assert isinstance(results_reordered[4], Item)
assert results_reordered[4].value == {"data": "value1"}
@@ -146,26 +110,9 @@ async def test_abatch_order(store: AsyncPostgresStore) -> None:
async def test_batch_get_ops(store: AsyncPostgresStore) -> None:
- mock_connection = store.conn
- mock_cursor = MockAsyncCursor(
- [
- {
- "key": "key1",
- "value": '{"data": "value1"}',
- "created_at": datetime.now(),
- "updated_at": datetime.now(),
- "prefix": "test.foo",
- },
- {
- "key": "key2",
- "value": '{"data": "value2"}',
- "created_at": datetime.now(),
- "updated_at": datetime.now(),
- "prefix": "test.bar",
- },
- ]
- )
- mock_connection.cursor.return_value = mock_cursor
+ # Setup test data
+ await store.aput(("test",), "key1", {"data": "value1"})
+ await store.aput(("test",), "key2", {"data": "value2"})
ops = [
GetOp(namespace=("test",), key="key1"),
@@ -184,10 +131,6 @@ async def test_batch_get_ops(store: AsyncPostgresStore) -> None:
async def test_batch_put_ops(store: AsyncPostgresStore) -> None:
- mock_connection = store.conn
- mock_cursor = MockAsyncCursor([])
- mock_connection.cursor.return_value = mock_cursor
-
ops = [
PutOp(namespace=("test",), key="key1", value={"data": "value1"}),
PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
@@ -198,30 +141,16 @@ async def test_batch_put_ops(store: AsyncPostgresStore) -> None:
assert len(results) == 3
assert all(result is None for result in results)
- assert mock_cursor.execute.call_count == 2
+
+ # Verify the puts worked
+ items = await store.asearch(["test"], limit=10)
+ assert len(items) == 2 # key3 had None value so wasn't stored
async def test_batch_search_ops(store: AsyncPostgresStore) -> None:
- mock_connection = store.conn
- mock_cursor = MockAsyncCursor(
- [
- {
- "key": "key1",
- "value": '{"data": "value1"}',
- "created_at": datetime.now(),
- "updated_at": datetime.now(),
- "prefix": "test.foo",
- },
- {
- "key": "key2",
- "value": '{"data": "value2"}',
- "created_at": datetime.now(),
- "updated_at": datetime.now(),
- "prefix": "test.bar",
- },
- ]
- )
- mock_connection.cursor.return_value = mock_cursor
+ # Setup test data
+ await store.aput(("test", "foo"), "key1", {"data": "value1"})
+ await store.aput(("test", "bar"), "key2", {"data": "value2"})
ops = [
SearchOp(
@@ -233,29 +162,23 @@ async def test_batch_search_ops(store: AsyncPostgresStore) -> None:
results = await store.abatch(ops)
assert len(results) == 2
- assert len(results[0]) == 2
- assert len(results[1]) == 2
+ assert len(results[0]) == 1 # Filtered results
+ assert len(results[1]) == 2 # All results
async def test_batch_list_namespaces_ops(store: AsyncPostgresStore) -> None:
- mock_connection = store.conn
- mock_cursor = MockAsyncCursor(
- [
- {"truncated_prefix": b"\x01test.namespace1"},
- {"truncated_prefix": b"\x01test.namespace2"},
- ]
- )
- mock_connection.cursor.return_value = mock_cursor
+ # Setup test data
+ await store.aput(("test", "namespace1"), "key1", {"data": "value1"})
+ await store.aput(("test", "namespace2"), "key2", {"data": "value2"})
ops = [ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0)]
results = await store.abatch(ops)
assert len(results) == 1
- assert results[0] == [("test", "namespace1"), ("test", "namespace2")]
-
-
-# The following use the actual DB connection
+ assert len(results[0]) == 2
+ assert ("test", "namespace1") in results[0]
+ assert ("test", "namespace2") in results[0]
class TestAsyncPostgresStore:
diff --git a/libs/checkpoint-postgres/tests/test_store.py b/libs/checkpoint-postgres/tests/test_store.py
index add9fb1c5..645c37e9f 100644
--- a/libs/checkpoint-postgres/tests/test_store.py
+++ b/libs/checkpoint-postgres/tests/test_store.py
@@ -1,174 +1,118 @@
# type: ignore
-import uuid
-from datetime import datetime
-from typing import Any
-from unittest.mock import MagicMock
+
+from uuid import uuid4
import pytest
from conftest import DEFAULT_URI # type: ignore
+from psycopg import Connection
-from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
+from langgraph.store.base import (
+ GetOp,
+ Item,
+ ListNamespacesOp,
+ MatchCondition,
+ PutOp,
+ SearchOp,
+)
from langgraph.store.postgres import PostgresStore
-class MockCursor:
- def __init__(self, fetch_result: Any) -> None:
- self.fetch_result = fetch_result
- self.execute = MagicMock()
- self.fetchall = MagicMock(return_value=self.fetch_result)
+@pytest.fixture(scope="function", params=["default", "pipe", "pool"])
+def store(request) -> PostgresStore:
+ database = f"test_{uuid4().hex[:16]}"
+ uri_parts = DEFAULT_URI.split("/")
+ uri_base = "/".join(uri_parts[:-1])
+ query_params = ""
+ if "?" in uri_parts[-1]:
+ db_name, query_params = uri_parts[-1].split("?", 1)
+ query_params = "?" + query_params
+ conn_string = f"{uri_base}/{database}{query_params}"
+ admin_conn_string = DEFAULT_URI
-class MockConnection:
- def __init__(self) -> None:
- self.cursor = MagicMock()
- self.pipeline = MagicMock()
+ with Connection.connect(admin_conn_string, autocommit=True) as conn:
+ conn.execute(f"CREATE DATABASE {database}")
+ try:
+ with PostgresStore.from_conn_string(conn_string) as store:
+ store.setup()
-
-@pytest.fixture
-def mock_connection() -> MockConnection:
- return MockConnection()
-
-
-@pytest.fixture
-def store(mock_connection: MockConnection) -> PostgresStore:
- return PostgresStore(mock_connection)
+ if request.param == "pipe":
+ with PostgresStore.from_conn_string(conn_string, pipeline=True) as store:
+ yield store
+ elif request.param == "pool":
+ with PostgresStore.from_conn_string(
+ conn_string, pool_config={"min_size": 1, "max_size": 10}
+ ) as store:
+ yield store
+ else: # default
+ with PostgresStore.from_conn_string(conn_string) as store:
+ yield store
+ finally:
+ with Connection.connect(admin_conn_string, autocommit=True) as conn:
+ conn.execute(f"DROP DATABASE {database}")
def test_batch_order(store: PostgresStore) -> None:
- mock_connection = store.conn
- mock_get_cursor = MockCursor(
- [
- {
- "key": "key1",
- "value": '{"data": "value1"}',
- "created_at": datetime.now(),
- "updated_at": datetime.now(),
- "prefix": "test.foo",
- },
- {
- "key": "key2",
- "value": '{"data": "value2"}',
- "created_at": datetime.now(),
- "updated_at": datetime.now(),
- "prefix": "test.bar",
- },
- ]
- )
- mock_search_cursor = MockCursor(
- [
- {
- "key": "key1",
- "value": '{"data": "value1"}',
- "created_at": datetime.now(),
- "updated_at": datetime.now(),
- "prefix": "test.foo",
- },
- ]
- )
- mock_list_namespaces_cursor = MockCursor(
- [
- {"truncated_prefix": b"\x01test"},
- ]
- )
-
- failures = []
-
- def cursor_side_effect(binary: bool = False) -> Any:
- cursor = MagicMock()
-
- def execute_side_effect(query: str, *params: Any) -> None:
- # My super sophisticated database.
- if "SELECT prefix, key, value" in query:
- cursor.fetchall = mock_search_cursor.fetchall
- elif "SELECT DISTINCT ON (truncated_prefix)" in query:
- cursor.fetchall = mock_list_namespaces_cursor.fetchall
- elif "WHERE prefix = %s AND key" in query:
- cursor.fetchall = mock_get_cursor.fetchall
- elif "INSERT INTO " in query:
- pass
- else:
- e = ValueError(f"Unmatched query: {query}")
- failures.append(e)
- raise e
-
- cursor.execute = MagicMock(side_effect=execute_side_effect)
- return cursor
-
- mock_connection.cursor.side_effect = cursor_side_effect
+ # Setup test data
+ store.put(("test", "foo"), "key1", {"data": "value1"})
+ store.put(("test", "bar"), "key2", {"data": "value2"})
ops = [
- GetOp(namespace=("test",), key="key1"),
- PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
+ GetOp(namespace=("test", "foo"), key="key1"),
+ PutOp(namespace=("test", "bar"), key="key2", value={"data": "value2"}),
SearchOp(
namespace_prefix=("test",), filter={"data": "value1"}, limit=10, offset=0
),
ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0),
GetOp(namespace=("test",), key="key3"),
]
+
results = store.batch(ops)
- assert not failures
assert len(results) == 5
assert isinstance(results[0], Item)
assert isinstance(results[0].value, dict)
assert results[0].value == {"data": "value1"}
assert results[0].key == "key1"
- assert results[1] is None
+ assert results[1] is None # Put operation returns None
assert isinstance(results[2], list)
assert len(results[2]) == 1
assert isinstance(results[3], list)
- assert results[3] == [("test",)]
- assert results[4] is None
+ assert len(results[3]) > 0 # Should contain at least our test namespaces
+ assert results[4] is None # Non-existent key returns None
+ # Test reordered operations
ops_reordered = [
SearchOp(namespace_prefix=("test",), filter=None, limit=5, offset=0),
- GetOp(namespace=("test",), key="key2"),
+ GetOp(namespace=("test", "bar"), key="key2"),
ListNamespacesOp(match_conditions=None, max_depth=None, limit=5, offset=0),
PutOp(namespace=("test",), key="key3", value={"data": "value3"}),
- GetOp(namespace=("test",), key="key1"),
+ GetOp(namespace=("test", "foo"), key="key1"),
]
results_reordered = store.batch(ops_reordered)
- assert not failures
assert len(results_reordered) == 5
assert isinstance(results_reordered[0], list)
- assert len(results_reordered[0]) == 1
+ assert len(results_reordered[0]) >= 2 # Should find at least our two test items
assert isinstance(results_reordered[1], Item)
assert results_reordered[1].value == {"data": "value2"}
assert results_reordered[1].key == "key2"
assert isinstance(results_reordered[2], list)
- assert results_reordered[2] == [("test",)]
- assert results_reordered[3] is None
+ assert len(results_reordered[2]) > 0
+ assert results_reordered[3] is None # Put operation returns None
assert isinstance(results_reordered[4], Item)
assert results_reordered[4].value == {"data": "value1"}
assert results_reordered[4].key == "key1"
def test_batch_get_ops(store: PostgresStore) -> None:
- mock_connection = store.conn
- mock_cursor = MockCursor(
- [
- {
- "key": "key1",
- "value": '{"data": "value1"}',
- "created_at": datetime.now(),
- "updated_at": datetime.now(),
- "prefix": "test.foo",
- },
- {
- "key": "key2",
- "value": '{"data": "value2"}',
- "created_at": datetime.now(),
- "updated_at": datetime.now(),
- "prefix": "test.bar",
- },
- ]
- )
- mock_connection.cursor.return_value = mock_cursor
+ # Setup test data
+ store.put(("test",), "key1", {"data": "value1"})
+ store.put(("test",), "key2", {"data": "value2"})
ops = [
GetOp(namespace=("test",), key="key1"),
GetOp(namespace=("test",), key="key2"),
- GetOp(namespace=("test",), key="key3"),
+ GetOp(namespace=("test",), key="key3"), # Non-existent key
]
results = store.batch(ops)
@@ -182,75 +126,90 @@ def test_batch_get_ops(store: PostgresStore) -> None:
def test_batch_put_ops(store: PostgresStore) -> None:
- mock_connection = store.conn
- mock_cursor = MockCursor([])
- mock_connection.cursor.return_value = mock_cursor
-
ops = [
PutOp(namespace=("test",), key="key1", value={"data": "value1"}),
PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
- PutOp(namespace=("test",), key="key3", value=None),
+ PutOp(namespace=("test",), key="key3", value=None), # Delete operation
]
results = store.batch(ops)
-
assert len(results) == 3
assert all(result is None for result in results)
- assert mock_cursor.execute.call_count == 2
+
+ # Verify the puts worked
+ item1 = store.get(("test",), "key1")
+ item2 = store.get(("test",), "key2")
+ item3 = store.get(("test",), "key3")
+
+ assert item1 and item1.value == {"data": "value1"}
+ assert item2 and item2.value == {"data": "value2"}
+ assert item3 is None
def test_batch_search_ops(store: PostgresStore) -> None:
- mock_connection = store.conn
- mock_cursor = MockCursor(
- [
- {
- "key": "key1",
- "value": '{"data": "value1"}',
- "created_at": datetime.now(),
- "updated_at": datetime.now(),
- "prefix": "test.foo",
- },
- {
- "key": "key2",
- "value": '{"data": "value2"}',
- "created_at": datetime.now(),
- "updated_at": datetime.now(),
- "prefix": "test.bar",
- },
- ]
- )
- mock_connection.cursor.return_value = mock_cursor
+ # Setup test data
+ test_data = [
+ (("test", "foo"), "key1", {"data": "value1", "tag": "a"}),
+ (("test", "bar"), "key2", {"data": "value2", "tag": "a"}),
+ (("test", "baz"), "key3", {"data": "value3", "tag": "b"}),
+ ]
+ for namespace, key, value in test_data:
+ store.put(namespace, key, value)
ops = [
- SearchOp(
- namespace_prefix=("test",), filter={"data": "value1"}, limit=10, offset=0
- ),
- SearchOp(namespace_prefix=("test",), filter=None, limit=5, offset=0),
+ SearchOp(namespace_prefix=("test",), filter={"tag": "a"}, limit=10, offset=0),
+ SearchOp(namespace_prefix=("test",), filter=None, limit=2, offset=0),
+ SearchOp(namespace_prefix=("test", "foo"), filter=None, limit=10, offset=0),
]
results = store.batch(ops)
+ assert len(results) == 3
- assert len(results) == 2
+ # First search should find items with tag "a"
assert len(results[0]) == 2
+ assert all(item.value["tag"] == "a" for item in results[0])
+
+ # Second search should return first 2 items
assert len(results[1]) == 2
+ # Third search should only find items in test/foo namespace
+ assert len(results[2]) == 1
+ assert results[2][0].namespace == ("test", "foo")
+
def test_batch_list_namespaces_ops(store: PostgresStore) -> None:
- mock_connection = store.conn
- mock_cursor = MockCursor(
- [
- {"truncated_prefix": b"\x01test.namespace1"},
- {"truncated_prefix": b"\x01test.namespace2"},
- ]
- )
- mock_connection.cursor.return_value = mock_cursor
+ # Setup test data with various namespaces
+ test_data = [
+ (("test", "documents", "public"), "doc1", {"content": "public doc"}),
+ (("test", "documents", "private"), "doc2", {"content": "private doc"}),
+ (("test", "images", "public"), "img1", {"content": "public image"}),
+ (("prod", "documents", "public"), "doc3", {"content": "prod doc"}),
+ ]
+ for namespace, key, value in test_data:
+ store.put(namespace, key, value)
- ops = [ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0)]
+ ops = [
+ ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0),
+ ListNamespacesOp(match_conditions=None, max_depth=2, limit=10, offset=0),
+ ListNamespacesOp(
+ match_conditions=[MatchCondition("suffix", "public")],
+ max_depth=None,
+ limit=10,
+ offset=0,
+ ),
+ ]
results = store.batch(ops)
+ assert len(results) == 3
- assert len(results) == 1
- assert results[0] == [("test", "namespace1"), ("test", "namespace2")]
+ # First operation should list all namespaces
+ assert len(results[0]) == len(test_data)
+
+ # Second operation should only return namespaces up to depth 2
+ assert all(len(ns) <= 2 for ns in results[1])
+
+ # Third operation should only return namespaces ending with "public"
+ assert all(ns[-1] == "public" for ns in results[2])
class TestPostgresStore:
@@ -273,195 +232,111 @@ class TestPostgresStore:
assert item.key == item_id
assert item.value == item_value
- updated_value = {
- "title": "Updated Test Document",
- "content": "Hello, LangGraph!",
- }
+ # Test update
+ updated_value = {"title": "Updated Document", "content": "Hello, Updated!"}
store.put(namespace, item_id, updated_value)
updated_item = store.get(namespace, item_id)
assert updated_item.value == updated_value
assert updated_item.updated_at > item.updated_at
+
+ # Test get from non-existent namespace
different_namespace = ("test", "other_documents")
item_in_different_namespace = store.get(different_namespace, item_id)
assert item_in_different_namespace is None
- new_item_id = "doc2"
- new_item_value = {"title": "Another Document", "content": "Greetings!"}
- store.put(namespace, new_item_id, new_item_value)
-
- search_results = store.search(["test"], limit=10)
- items = search_results
- assert len(items) == 2
- assert any(item.key == item_id for item in items)
- assert any(item.key == new_item_id for item in items)
-
- namespaces = store.list_namespaces(prefix=["test"])
- assert ("test", "documents") in namespaces
-
+ # Test delete
store.delete(namespace, item_id)
- store.delete(namespace, new_item_id)
deleted_item = store.get(namespace, item_id)
assert deleted_item is None
- deleted_item = store.get(namespace, new_item_id)
- assert deleted_item is None
-
- empty_search_results = store.search(["test"], limit=10)
- assert len(empty_search_results) == 0
-
def test_list_namespaces(self) -> None:
with PostgresStore.from_conn_string(DEFAULT_URI) as store:
- test_pref = str(uuid.uuid4())
+ # Create test data with various namespaces
test_namespaces = [
- (test_pref, "test", "documents", "public", test_pref),
- (test_pref, "test", "documents", "private", test_pref),
- (test_pref, "test", "images", "public", test_pref),
- (test_pref, "test", "images", "private", test_pref),
- (test_pref, "prod", "documents", "public", test_pref),
- (
- test_pref,
- "prod",
- "documents",
- "some",
- "nesting",
- "public",
- test_pref,
- ),
- (test_pref, "prod", "documents", "private", test_pref),
+ ("test", "documents", "public"),
+ ("test", "documents", "private"),
+ ("test", "images", "public"),
+ ("test", "images", "private"),
+ ("prod", "documents", "public"),
+ ("prod", "documents", "private"),
]
+ # Insert test data
for namespace in test_namespaces:
store.put(namespace, "dummy", {"content": "dummy"})
- prefix_result = store.list_namespaces(prefix=[test_pref, "test"])
- assert len(prefix_result) == 4
- assert all([ns[1] == "test" for ns in prefix_result])
+ # Test listing with various filters
+ all_namespaces = store.list_namespaces()
+ assert len(all_namespaces) == len(test_namespaces)
- specific_prefix_result = store.list_namespaces(
- prefix=[test_pref, "test", "documents"]
- )
- assert len(specific_prefix_result) == 2
- assert all(
- [ns[1:3] == ("test", "documents") for ns in specific_prefix_result]
- )
+ # Test prefix filtering
+ test_prefix_namespaces = store.list_namespaces(prefix=["test"])
+ assert len(test_prefix_namespaces) == 4
+ assert all(ns[0] == "test" for ns in test_prefix_namespaces)
- suffix_result = store.list_namespaces(suffix=["public", test_pref])
- assert len(suffix_result) == 4
- assert all(ns[-2] == "public" for ns in suffix_result)
+ # Test suffix filtering
+ public_namespaces = store.list_namespaces(suffix=["public"])
+ assert len(public_namespaces) == 3
+ assert all(ns[-1] == "public" for ns in public_namespaces)
- prefix_suffix_result = store.list_namespaces(
- prefix=[test_pref, "test"], suffix=["public", test_pref]
- )
- assert len(prefix_suffix_result) == 2
- assert all(
- ns[1] == "test" and ns[-2] == "public" for ns in prefix_suffix_result
- )
+ # Test max depth
+ depth_2_namespaces = store.list_namespaces(max_depth=2)
+ assert all(len(ns) <= 2 for ns in depth_2_namespaces)
- wildcard_prefix_result = store.list_namespaces(
- prefix=[test_pref, "*", "documents"]
- )
- assert len(wildcard_prefix_result) == 5
- assert all(ns[2] == "documents" for ns in wildcard_prefix_result)
-
- wildcard_suffix_result = store.list_namespaces(
- suffix=["*", "public", test_pref]
- )
- assert len(wildcard_suffix_result) == 4
- assert all(ns[-2] == "public" for ns in wildcard_suffix_result)
- wildcard_single = store.list_namespaces(
- suffix=["some", "*", "public", test_pref]
- )
- assert len(wildcard_single) == 1
- assert wildcard_single[0] == (
- test_pref,
- "prod",
- "documents",
- "some",
- "nesting",
- "public",
- test_pref,
- )
-
- max_depth_result = store.list_namespaces(max_depth=3)
- assert all([len(ns) <= 3 for ns in max_depth_result])
-
- max_depth_result = store.list_namespaces(
- max_depth=4, prefix=[test_pref, "*", "documents"]
- )
- assert (
- len(set(tuple(res) for res in max_depth_result))
- == len(max_depth_result)
- == 5
- )
-
- limit_result = store.list_namespaces(prefix=[test_pref], limit=3)
- assert len(limit_result) == 3
-
- offset_result = store.list_namespaces(prefix=[test_pref], offset=3)
- assert len(offset_result) == len(test_namespaces) - 3
-
- empty_prefix_result = store.list_namespaces(prefix=[test_pref])
- assert len(empty_prefix_result) == len(test_namespaces)
- assert set(tuple(ns) for ns in empty_prefix_result) == set(
- tuple(ns) for ns in test_namespaces
- )
+ # Test pagination
+ paginated_namespaces = store.list_namespaces(limit=3)
+ assert len(paginated_namespaces) == 3
+ # Cleanup
for namespace in test_namespaces:
store.delete(namespace, "dummy")
- def test_search(self):
+ def test_search(self) -> None:
with PostgresStore.from_conn_string(DEFAULT_URI) as store:
- test_namespaces = [
- ("test_search", "documents", "user1"),
- ("test_search", "documents", "user2"),
- ("test_search", "reports", "department1"),
- ("test_search", "reports", "department2"),
- ]
- test_items = [
- {"title": "Doc 1", "author": "John Doe", "tags": ["important"]},
- {"title": "Doc 2", "author": "Jane Smith", "tags": ["draft"]},
- {"title": "Report A", "author": "John Doe", "tags": ["final"]},
- {"title": "Report B", "author": "Alice Johnson", "tags": ["draft"]},
+ # Create test data
+ test_data = [
+ (
+ ("test", "docs"),
+ "doc1",
+ {"title": "First Doc", "author": "Alice", "tags": ["important"]},
+ ),
+ (
+ ("test", "docs"),
+ "doc2",
+ {"title": "Second Doc", "author": "Bob", "tags": ["draft"]},
+ ),
+ (
+ ("test", "images"),
+ "img1",
+ {"title": "Image 1", "author": "Alice", "tags": ["final"]},
+ ),
]
- for namespace, item in zip(test_namespaces, test_items):
- store.put(namespace, f"item_{namespace[-1]}", item)
+ for namespace, key, value in test_data:
+ store.put(namespace, key, value)
- docs_result = store.search(["test_search", "documents"])
- assert len(docs_result) == 2
- assert all(
- [item.namespace[1] == "documents" for item in docs_result]
- ), docs_result
+ # Test basic search
+ all_items = store.search(["test"])
+ assert len(all_items) == 3
- reports_result = store.search(["test_search", "reports"])
- assert len(reports_result) == 2
- assert all(item.namespace[1] == "reports" for item in reports_result)
+ # Test namespace filtering
+ docs_items = store.search(["test", "docs"])
+ assert len(docs_items) == 2
+ assert all(item.namespace == ("test", "docs") for item in docs_items)
- limited_result = store.search(["test_search"], limit=2)
- assert len(limited_result) == 2
- offset_result = store.search(["test_search"])
- assert len(offset_result) == 4
+ # Test value filtering
+ alice_items = store.search(["test"], filter={"author": "Alice"})
+ assert len(alice_items) == 2
+ assert all(item.value["author"] == "Alice" for item in alice_items)
- offset_result = store.search(["test_search"], offset=2)
- assert len(offset_result) == 2
- assert all(item not in limited_result for item in offset_result)
+ # Test pagination
+ paginated_items = store.search(["test"], limit=2)
+ assert len(paginated_items) == 2
- john_doe_result = store.search(
- ["test_search"], filter={"author": "John Doe"}
- )
- assert len(john_doe_result) == 2
- assert all(item.value["author"] == "John Doe" for item in john_doe_result)
+ offset_items = store.search(["test"], offset=2)
+ assert len(offset_items) == 1
- draft_result = store.search(["test_search"], filter={"tags": ["draft"]})
- assert len(draft_result) == 2
- assert all("draft" in item.value["tags"] for item in draft_result)
-
- page1 = store.search(["test_search"], limit=2, offset=0)
- page2 = store.search(["test_search"], limit=2, offset=2)
- all_items = page1 + page2
- assert len(all_items) == 4
- assert len(set(item.key for item in all_items)) == 4
-
- for namespace in test_namespaces:
- store.delete(namespace, f"item_{namespace[-1]}")
+ # Cleanup
+ for namespace, key, _ in test_data:
+ store.delete(namespace, key)
diff --git a/libs/langgraph/Makefile b/libs/langgraph/Makefile
index 2aacf6db8..43d0c7afe 100644
--- a/libs/langgraph/Makefile
+++ b/libs/langgraph/Makefile
@@ -48,8 +48,13 @@ test:
make stop-postgres; \
exit $$EXIT_CODE
+WORKERS ?= auto
+XDIST_ARGS := $(if $(WORKERS),-n $(WORKERS) --dist worksteal,)
+MAXFAIL ?=
+MAXFAIL_ARGS := $(if $(MAXFAIL),--maxfail $(MAXFAIL),)
+
test_watch:
- make start-postgres && poetry run ptw . -- --ff -vv -x -n auto --dist worksteal --snapshot-update --tb short $(TEST); \
+ make start-postgres && poetry run ptw . -- --ff -vv -x $(XDIST_ARGS) $(MAXFAIL_ARGS) --snapshot-update --tb short $(TEST); \
EXIT_CODE=$$?; \
make stop-postgres; \
exit $$EXIT_CODE
diff --git a/libs/langgraph/tests/conftest.py b/libs/langgraph/tests/conftest.py
index eae7694ff..0381206e3 100644
--- a/libs/langgraph/tests/conftest.py
+++ b/libs/langgraph/tests/conftest.py
@@ -272,6 +272,54 @@ async def _store_postgres_aio():
await conn.execute(f"DROP DATABASE {database}")
+@asynccontextmanager
+async def _store_postgres_aio_pipe():
+ if sys.version_info < (3, 10):
+ pytest.skip("Async Postgres tests require Python 3.10+")
+ database = f"test_{uuid4().hex[:16]}"
+ async with await AsyncConnection.connect(
+ DEFAULT_POSTGRES_URI, autocommit=True
+ ) as conn:
+ await conn.execute(f"CREATE DATABASE {database}")
+ try:
+ async with AsyncPostgresStore.from_conn_string(
+ DEFAULT_POSTGRES_URI + database
+ ) as store:
+ await store.setup() # Run in its own transaction
+ async with AsyncPostgresStore.from_conn_string(
+ DEFAULT_POSTGRES_URI + database, pipeline=True
+ ) as store:
+ yield store
+ finally:
+ async with await AsyncConnection.connect(
+ DEFAULT_POSTGRES_URI, autocommit=True
+ ) as conn:
+ await conn.execute(f"DROP DATABASE {database}")
+
+
+@asynccontextmanager
+async def _store_postgres_aio_pool():
+ if sys.version_info < (3, 10):
+ pytest.skip("Async Postgres tests require Python 3.10+")
+ database = f"test_{uuid4().hex[:16]}"
+ async with await AsyncConnection.connect(
+ DEFAULT_POSTGRES_URI, autocommit=True
+ ) as conn:
+ await conn.execute(f"CREATE DATABASE {database}")
+ try:
+ async with AsyncPostgresStore.from_conn_string(
+ DEFAULT_POSTGRES_URI + database,
+ pool_config={"max_size": 10},
+ ) as store:
+ await store.setup()
+ yield store
+ finally:
+ async with await AsyncConnection.connect(
+ DEFAULT_POSTGRES_URI, autocommit=True
+ ) as conn:
+ await conn.execute(f"DROP DATABASE {database}")
+
+
@asynccontextmanager
async def _store_duckdb_aio():
async with AsyncDuckDBStore.from_conn_string(":memory:") as store:
@@ -296,6 +344,45 @@ def store_postgres():
conn.execute(f"DROP DATABASE {database}")
+@pytest.fixture(scope="function")
+def store_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 store
+ with PostgresStore.from_conn_string(DEFAULT_POSTGRES_URI + database) as store:
+ store.setup() # Run in its own transaction
+ with PostgresStore.from_conn_string(
+ DEFAULT_POSTGRES_URI + database, pipeline=True
+ ) as store:
+ yield store
+ 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 store_postgres_pool():
+ 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 store
+ with PostgresStore.from_conn_string(
+ DEFAULT_POSTGRES_URI + database, pool_config={"max_size": 10}
+ ) as store:
+ store.setup()
+ yield store
+ 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 store_duckdb():
with DuckDBStore.from_conn_string(":memory:") as store:
@@ -317,6 +404,12 @@ async def awith_store(store_name: Optional[str]) -> AsyncIterator[BaseStore]:
elif store_name == "postgres_aio":
async with _store_postgres_aio() as store:
yield store
+ elif store_name == "postgres_aio_pipe":
+ async with _store_postgres_aio_pipe() as store:
+ yield store
+ elif store_name == "postgres_aio_pool":
+ async with _store_postgres_aio_pool() as store:
+ yield store
elif store_name == "duckdb_aio":
async with _store_duckdb_aio() as store:
yield store
@@ -342,5 +435,17 @@ ALL_CHECKPOINTERS_ASYNC_PLUS_NONE = [
*ALL_CHECKPOINTERS_ASYNC,
None,
]
-ALL_STORES_SYNC = ["in_memory", "postgres", "duckdb"]
-ALL_STORES_ASYNC = ["in_memory", "postgres_aio", "duckdb_aio"]
+ALL_STORES_SYNC = [
+ "in_memory",
+ "postgres",
+ "postgres_pipe",
+ "postgres_pool",
+ "duckdb",
+]
+ALL_STORES_ASYNC = [
+ "in_memory",
+ "postgres_aio",
+ "postgres_aio_pipe",
+ "postgres_aio_pool",
+ "duckdb_aio",
+]
diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py
index fda881b15..fcee1fa20 100644
--- a/libs/langgraph/tests/test_pregel.py
+++ b/libs/langgraph/tests/test_pregel.py
@@ -1,5 +1,6 @@
import enum
import json
+import logging
import operator
import re
import time
@@ -67,16 +68,9 @@ from langgraph.errors import InvalidUpdateError, MultipleSubgraphsError, NodeInt
from langgraph.graph import END, Graph, GraphCommand, StateGraph
from langgraph.graph.message import MessageGraph, MessagesState, add_messages
from langgraph.managed.shared_value import SharedValue
-from langgraph.prebuilt.chat_agent_executor import (
- create_tool_calling_executor,
-)
+from langgraph.prebuilt.chat_agent_executor import create_tool_calling_executor
from langgraph.prebuilt.tool_node import ToolNode
-from langgraph.pregel import (
- Channel,
- GraphRecursionError,
- Pregel,
- StateSnapshot,
-)
+from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot
from langgraph.pregel.retry import RetryPolicy
from langgraph.store.base import BaseStore
from langgraph.store.memory import InMemoryStore
@@ -104,6 +98,8 @@ from tests.messages import (
_AnyIdToolMessage,
)
+logger = logging.getLogger(__name__)
+
# define these objects to avoid importing langchain_core.agents
# and therefore avoid relying on core Pydantic version
@@ -6628,11 +6624,7 @@ def test_message_graph(
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
- from langchain_core.messages import (
- AIMessage,
- BaseMessage,
- HumanMessage,
- )
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
from langchain_core.outputs import ChatGeneration, ChatResult
from langchain_core.tools import tool
@@ -13937,50 +13929,75 @@ def test_store_injected(
doc_id = str(uuid.uuid4())
doc = {"some-key": "this-is-a-val"}
-
- def node(input: State, config: RunnableConfig, store: BaseStore):
- assert isinstance(store, BaseStore)
- store.put(
- ("foo", "bar"),
- doc_id,
- {
- **doc,
- "from_thread": config["configurable"]["thread_id"],
- "some_val": input["count"],
- },
- )
- return {"count": 1}
-
- builder = StateGraph(State)
- builder.add_node("node", node)
- builder.add_edge("__start__", "node")
- graph = builder.compile(store=the_store, checkpointer=checkpointer)
-
+ uid = uuid.uuid4().hex
+ namespace = (f"foo-{uid}", "bar")
thread_1 = str(uuid.uuid4())
- result = graph.invoke({"count": 0}, {"configurable": {"thread_id": thread_1}})
- assert result == {"count": 1}
- returned_doc = the_store.get(("foo", "bar"), doc_id).value
- assert returned_doc == {**doc, "from_thread": thread_1, "some_val": 0}
- assert len(the_store.search(("foo", "bar"))) == 1
-
- # Check update on existing thread
- result = graph.invoke({"count": 0}, {"configurable": {"thread_id": thread_1}})
- assert result == {"count": 2}
- returned_doc = the_store.get(("foo", "bar"), doc_id).value
- assert returned_doc == {**doc, "from_thread": thread_1, "some_val": 1}
- assert len(the_store.search(("foo", "bar"))) == 1
-
thread_2 = str(uuid.uuid4())
+ class Node:
+ def __init__(self, i: Optional[int] = None):
+ self.i = i
+
+ def __call__(self, inputs: State, config: RunnableConfig, store: BaseStore):
+ assert isinstance(store, BaseStore)
+ store.put(
+ namespace
+ if self.i is not None
+ and config["configurable"]["thread_id"] in (thread_1, thread_2)
+ else (f"foo_{self.i}", "bar"),
+ doc_id,
+ {
+ **doc,
+ "from_thread": config["configurable"]["thread_id"],
+ "some_val": inputs["count"],
+ },
+ )
+ return {"count": 1}
+
+ builder = StateGraph(State)
+ builder.add_node("node", Node())
+ builder.add_edge("__start__", "node")
+ N = 500
+ M = 1
+ if "duckdb" in store_name:
+ logger.warning(
+ "DuckDB store implementation has a known issue that does not"
+ " support concurrent writes, so we're reducing the test scope"
+ )
+ N = M = 1
+
+ for i in range(N):
+ builder.add_node(f"node_{i}", Node(i))
+ builder.add_edge("__start__", f"node_{i}")
+
+ graph = builder.compile(store=the_store, checkpointer=checkpointer)
+
+ results = graph.batch(
+ [{"count": 0}] * M,
+ ([{"configurable": {"thread_id": str(uuid.uuid4())}}] * (M - 1))
+ + [{"configurable": {"thread_id": thread_1}}],
+ )
+ result = results[-1]
+ assert result == {"count": N + 1}
+ returned_doc = the_store.get(namespace, doc_id).value
+ assert returned_doc == {**doc, "from_thread": thread_1, "some_val": 0}
+ assert len(the_store.search(namespace)) == 1
+ # Check results after another turn of the same thread
+ result = graph.invoke({"count": 0}, {"configurable": {"thread_id": thread_1}})
+ assert result == {"count": (N + 1) * 2}
+ returned_doc = the_store.get(namespace, doc_id).value
+ assert returned_doc == {**doc, "from_thread": thread_1, "some_val": N + 1}
+ assert len(the_store.search(namespace)) == 1
+
result = graph.invoke({"count": 0}, {"configurable": {"thread_id": thread_2}})
- assert result == {"count": 1}
- returned_doc = the_store.get(("foo", "bar"), doc_id).value
+ assert result == {"count": N + 1}
+ returned_doc = the_store.get(namespace, doc_id).value
assert returned_doc == {
**doc,
"from_thread": thread_2,
"some_val": 0,
} # Overwrites the whole doc
- assert len(the_store.search(("foo", "bar"))) == 1 # still overwriting the same one
+ assert len(the_store.search(namespace)) == 1 # still overwriting the same one
def test_enum_node_names():
diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py
index a31e444e1..c5812e896 100644
--- a/libs/langgraph/tests/test_pregel_async.py
+++ b/libs/langgraph/tests/test_pregel_async.py
@@ -1,4 +1,5 @@
import asyncio
+import logging
import operator
import random
import re
@@ -100,6 +101,8 @@ from tests.messages import (
_AnyIdToolMessage,
)
+logger = logging.getLogger(__name__)
+
pytestmark = pytest.mark.anyio
@@ -12272,60 +12275,89 @@ async def test_store_injected_async(checkpointer_name: str, store_name: str) ->
doc_id = str(uuid.uuid4())
doc = {"some-key": "this-is-a-val"}
+ uid = uuid.uuid4().hex
+ namespace = (f"foo-{uid}", "bar")
+ thread_1 = str(uuid.uuid4())
+ thread_2 = str(uuid.uuid4())
- async def node(input: State, config: RunnableConfig, store: BaseStore):
- assert isinstance(store, BaseStore)
- await store.aput(
- ("foo", "bar"),
- doc_id,
- {
- **doc,
- "from_thread": config["configurable"]["thread_id"],
- "some_val": input["count"],
- },
- )
- return {"count": 1}
+ class Node:
+ def __init__(self, i: Optional[int] = None):
+ self.i = i
+
+ async def __call__(
+ self, inputs: State, config: RunnableConfig, store: BaseStore
+ ):
+ assert isinstance(store, BaseStore)
+ await store.aput(
+ namespace
+ if self.i is not None
+ and config["configurable"]["thread_id"] in (thread_1, thread_2)
+ else (f"foo_{self.i}", "bar"),
+ doc_id,
+ {
+ **doc,
+ "from_thread": config["configurable"]["thread_id"],
+ "some_val": inputs["count"],
+ },
+ )
+ return {"count": 1}
builder = StateGraph(State)
- builder.add_node("node", node)
+ builder.add_node("node", Node())
builder.add_edge("__start__", "node")
+
+ N = 500
+ M = 1
+ if "duckdb" in store_name:
+ logger.warning(
+ "DuckDB store implementation has a known issue that does not"
+ " support concurrent writes, so we're reducing the test scope"
+ )
+ N = M = 1
+
+ for i in range(N):
+ builder.add_node(f"node_{i}", Node(i))
+ builder.add_edge("__start__", f"node_{i}")
+
async with awith_checkpointer(checkpointer_name) as checkpointer, awith_store(
store_name
) as the_store:
graph = builder.compile(store=the_store, checkpointer=checkpointer)
- thread_1 = str(uuid.uuid4())
- result = await graph.ainvoke(
- {"count": 0}, {"configurable": {"thread_id": thread_1}}
+ # Test batch operations with multiple threads
+ results = await graph.abatch(
+ [{"count": 0}] * M,
+ ([{"configurable": {"thread_id": str(uuid.uuid4())}}] * (M - 1))
+ + [{"configurable": {"thread_id": thread_1}}],
)
- assert result == {"count": 1}
- returned_doc = (await the_store.aget(("foo", "bar"), doc_id)).value
+ result = results[-1]
+ assert result == {"count": N + 1}
+ returned_doc = (await the_store.aget(namespace, doc_id)).value
assert returned_doc == {**doc, "from_thread": thread_1, "some_val": 0}
- assert len((await the_store.asearch(("foo", "bar")))) == 1
+ assert len((await the_store.asearch(namespace))) == 1
- # Check update on existing thread
+ # Check results after another turn of the same thread
result = await graph.ainvoke(
{"count": 0}, {"configurable": {"thread_id": thread_1}}
)
- assert result == {"count": 2}
- returned_doc = (await the_store.aget(("foo", "bar"), doc_id)).value
- assert returned_doc == {**doc, "from_thread": thread_1, "some_val": 1}
- assert len((await the_store.asearch(("foo", "bar")))) == 1
-
- thread_2 = str(uuid.uuid4())
+ assert result == {"count": (N + 1) * 2}
+ returned_doc = (await the_store.aget(namespace, doc_id)).value
+ assert returned_doc == {**doc, "from_thread": thread_1, "some_val": N + 1}
+ assert len((await the_store.asearch(namespace))) == 1
+ # Test with a different thread
result = await graph.ainvoke(
{"count": 0}, {"configurable": {"thread_id": thread_2}}
)
- assert result == {"count": 1}
- returned_doc = (await the_store.aget(("foo", "bar"), doc_id)).value
+ assert result == {"count": N + 1}
+ returned_doc = (await the_store.aget(namespace, doc_id)).value
assert returned_doc == {
**doc,
"from_thread": thread_2,
"some_val": 0,
} # Overwrites the whole doc
assert (
- len((await the_store.asearch(("foo", "bar")))) == 1
+ len((await the_store.asearch(namespace))) == 1
) # still overwriting the same one
From 8e1cd0e225282f39050f2d92cfd8cb01d4138560 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Mon, 25 Nov 2024 14:11:20 -0800
Subject: [PATCH 045/149] Add test
---
libs/langgraph/tests/test_pregel.py | 76 +++++++++++++++++++++
libs/langgraph/tests/test_pregel_async.py | 80 +++++++++++++++++++++++
2 files changed, 156 insertions(+)
diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py
index fda881b15..13a0e6643 100644
--- a/libs/langgraph/tests/test_pregel.py
+++ b/libs/langgraph/tests/test_pregel.py
@@ -14378,3 +14378,79 @@ def test_runnable_passthrough_node_graph() -> None:
graph = graph_builder.compile()
assert graph.get_graph(xray=True).to_json() == graph.get_graph(xray=False).to_json()
+
+
+@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
+def test_parent_command(request: pytest.FixtureRequest, checkpointer_name: str) -> None:
+ from langchain_core.messages import BaseMessage
+ from langchain_core.tools import tool
+
+ @tool(return_direct=True)
+ def get_user_name() -> GraphCommand:
+ """Retrieve user name"""
+ return GraphCommand(update={"user_name": "Meow"}, graph=GraphCommand.PARENT)
+
+ subgraph_builder = StateGraph(MessagesState)
+ subgraph_builder.add_node("tool", get_user_name)
+ subgraph_builder.add_edge(START, "tool")
+ subgraph = subgraph_builder.compile()
+
+ class CustomParentState(TypedDict):
+ messages: Annotated[list[BaseMessage], add_messages]
+ # this key is not available to the child graph
+ user_name: str
+
+ builder = StateGraph(CustomParentState)
+ builder.add_node("alice", subgraph)
+ builder.add_edge(START, "alice")
+ checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
+ graph = builder.compile(checkpointer=checkpointer)
+
+ config = {"configurable": {"thread_id": "1"}}
+
+ assert graph.invoke({"messages": [("user", "get user name")]}, config) == {
+ "messages": [
+ _AnyIdHumanMessage(
+ content="get user name", additional_kwargs={}, response_metadata={}
+ ),
+ ],
+ "user_name": "Meow",
+ }
+ assert graph.get_state(config) == StateSnapshot(
+ values={
+ "messages": [
+ _AnyIdHumanMessage(
+ content="get user name", additional_kwargs={}, response_metadata={}
+ ),
+ ],
+ "user_name": "Meow",
+ },
+ next=(),
+ config={
+ "configurable": {
+ "thread_id": "1",
+ "checkpoint_ns": "",
+ "checkpoint_id": AnyStr(),
+ }
+ },
+ metadata={
+ "source": "loop",
+ "writes": {
+ "alice": {
+ "user_name": "Meow",
+ }
+ },
+ "thread_id": "1",
+ "step": 1,
+ "parents": {},
+ },
+ created_at=AnyStr(),
+ parent_config={
+ "configurable": {
+ "thread_id": "1",
+ "checkpoint_ns": "",
+ "checkpoint_id": AnyStr(),
+ }
+ },
+ tasks=(),
+ )
diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py
index a31e444e1..12469f893 100644
--- a/libs/langgraph/tests/test_pregel_async.py
+++ b/libs/langgraph/tests/test_pregel_async.py
@@ -12565,3 +12565,83 @@ async def test_debug_nested_subgraphs():
assert stream_task["interrupts"] == history_task.interrupts
assert stream_task.get("error") == history_task.error
assert stream_task.get("state") == history_task.state
+
+
+@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
+async def test_parent_command(checkpointer_name: str) -> None:
+ from langchain_core.messages import BaseMessage
+ from langchain_core.tools import tool
+
+ @tool(return_direct=True)
+ def get_user_name() -> GraphCommand:
+ """Retrieve user name"""
+ return GraphCommand(update={"user_name": "Meow"}, graph=GraphCommand.PARENT)
+
+ subgraph_builder = StateGraph(MessagesState)
+ subgraph_builder.add_node("tool", get_user_name)
+ subgraph_builder.add_edge(START, "tool")
+ subgraph = subgraph_builder.compile()
+
+ class CustomParentState(TypedDict):
+ messages: Annotated[list[BaseMessage], add_messages]
+ # this key is not available to the child graph
+ user_name: str
+
+ builder = StateGraph(CustomParentState)
+ builder.add_node("alice", subgraph)
+ builder.add_edge(START, "alice")
+ async with awith_checkpointer(checkpointer_name) as checkpointer:
+ graph = builder.compile(checkpointer=checkpointer)
+
+ config = {"configurable": {"thread_id": "1"}}
+
+ assert await graph.ainvoke(
+ {"messages": [("user", "get user name")]}, config
+ ) == {
+ "messages": [
+ _AnyIdHumanMessage(
+ content="get user name", additional_kwargs={}, response_metadata={}
+ ),
+ ],
+ "user_name": "Meow",
+ }
+ assert await graph.aget_state(config) == StateSnapshot(
+ values={
+ "messages": [
+ _AnyIdHumanMessage(
+ content="get user name",
+ additional_kwargs={},
+ response_metadata={},
+ ),
+ ],
+ "user_name": "Meow",
+ },
+ next=(),
+ config={
+ "configurable": {
+ "thread_id": "1",
+ "checkpoint_ns": "",
+ "checkpoint_id": AnyStr(),
+ }
+ },
+ metadata={
+ "source": "loop",
+ "writes": {
+ "alice": {
+ "user_name": "Meow",
+ }
+ },
+ "thread_id": "1",
+ "step": 1,
+ "parents": {},
+ },
+ created_at=AnyStr(),
+ parent_config={
+ "configurable": {
+ "thread_id": "1",
+ "checkpoint_ns": "",
+ "checkpoint_id": AnyStr(),
+ }
+ },
+ tasks=(),
+ )
From 1febec7c0de72f90d7476666965b2a2ace66063b Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Mon, 25 Nov 2024 16:31:26 -0800
Subject: [PATCH 046/149] Dedup store batch operations (#2534)
---
libs/checkpoint/langgraph/store/base/batch.py | 78 +++++++++++++++-
libs/checkpoint/tests/test_store.py | 88 ++++++++++++++++---
2 files changed, 154 insertions(+), 12 deletions(-)
diff --git a/libs/checkpoint/langgraph/store/base/batch.py b/libs/checkpoint/langgraph/store/base/batch.py
index 079888222..b1030942d 100644
--- a/libs/checkpoint/langgraph/store/base/batch.py
+++ b/libs/checkpoint/langgraph/store/base/batch.py
@@ -6,6 +6,9 @@ from langgraph.store.base import (
BaseStore,
GetOp,
Item,
+ ListNamespacesOp,
+ MatchCondition,
+ NameSpacePath,
Op,
PutOp,
SearchOp,
@@ -68,6 +71,74 @@ class AsyncBatchedBaseStore(BaseStore):
self._aqueue[fut] = PutOp(namespace, key, None)
return await fut
+ async def alist_namespaces(
+ self,
+ *,
+ prefix: Optional[NameSpacePath] = None,
+ suffix: Optional[NameSpacePath] = None,
+ max_depth: Optional[int] = None,
+ limit: int = 100,
+ offset: int = 0,
+ ) -> list[tuple[str, ...]]:
+ fut = self._loop.create_future()
+ match_conditions = []
+ if prefix:
+ match_conditions.append(MatchCondition(match_type="prefix", path=prefix))
+ if suffix:
+ match_conditions.append(MatchCondition(match_type="suffix", path=suffix))
+
+ op = ListNamespacesOp(
+ match_conditions=tuple(match_conditions),
+ max_depth=max_depth,
+ limit=limit,
+ offset=offset,
+ )
+ self._aqueue[fut] = op
+ return await fut
+
+
+def _dedupe_ops(values: list[Op]) -> tuple[Optional[list[int]], list[Op]]:
+ """Dedupe operations while preserving order for results.
+
+ Args:
+ values: List of operations to dedupe
+
+ Returns:
+ Tuple of (listen indices, deduped operations)
+ where listen indices map deduped operation results back to original positions
+ """
+ if len(values) <= 1:
+ return None, list(values)
+
+ dedupped: list[Op] = []
+ listen: list[int] = []
+ puts: dict[tuple[tuple[str, ...], str], int] = {}
+
+ for op in values:
+ if isinstance(op, (GetOp, SearchOp, ListNamespacesOp)):
+ try:
+ listen.append(dedupped.index(op))
+ except ValueError:
+ listen.append(len(dedupped))
+ dedupped.append(op)
+ elif isinstance(op, PutOp):
+ putkey = (op.namespace, op.key)
+ if putkey in puts:
+ # Overwrite previous put
+ ix = puts[putkey]
+ dedupped[ix] = op
+ listen.append(ix)
+ else:
+ puts[putkey] = len(dedupped)
+ listen.append(len(dedupped))
+ dedupped.append(op)
+
+ else: # Any new ops will be treated regularly
+ listen.append(len(dedupped))
+ dedupped.append(op)
+
+ return listen, dedupped
+
async def _run(
aqueue: dict[asyncio.Future, Op], store: weakref.ReferenceType[BaseStore]
@@ -81,7 +152,12 @@ async def _run(
taken = aqueue.copy()
# action each operation
try:
- results = await s.abatch(taken.values())
+ values = list(taken.values())
+ listen, dedupped = _dedupe_ops(values)
+ results = await s.abatch(dedupped)
+ if listen is not None:
+ results = [results[ix] for ix in listen]
+
# set the results of each operation
for fut, result in zip(taken, results):
fut.set_result(result)
diff --git a/libs/checkpoint/tests/test_store.py b/libs/checkpoint/tests/test_store.py
index 9d06281d0..0ecd4bd84 100644
--- a/libs/checkpoint/tests/test_store.py
+++ b/libs/checkpoint/tests/test_store.py
@@ -10,6 +10,18 @@ from langgraph.store.base.batch import AsyncBatchedBaseStore
from langgraph.store.memory import InMemoryStore
+class MockAsyncBatchedStore(AsyncBatchedBaseStore):
+ def __init__(self) -> None:
+ super().__init__()
+ self._store = InMemoryStore()
+
+ def batch(self, ops: Iterable[Op]) -> list[Result]:
+ return self._store.batch(ops)
+
+ async def abatch(self, ops: Iterable[Op]) -> list[Result]:
+ return self._store.batch(ops)
+
+
async def test_async_batch_store(mocker: MockerFixture) -> None:
abatch = mocker.stub()
@@ -313,17 +325,6 @@ async def test_cannot_put_empty_namespace() -> None:
store.delete(("langgraph", "foo"), "bar")
assert store.get(("langgraph", "foo"), "bar") is None
- class MockAsyncBatchedStore(AsyncBatchedBaseStore):
- def __init__(self) -> None:
- super().__init__()
- self._store = InMemoryStore()
-
- def batch(self, ops: Iterable[Op]) -> list[Result]:
- return self._store.batch(ops)
-
- async def abatch(self, ops: Iterable[Op]) -> list[Result]:
- return self._store.batch(ops)
-
async_store = MockAsyncBatchedStore()
doc = {"foo": "bar"}
@@ -354,3 +355,68 @@ async def test_cannot_put_empty_namespace() -> None:
assert (await async_store.asearch(("valid", "namespace")))[0].value == doc
await async_store.adelete(("valid", "namespace"), "key")
assert (await async_store.aget(("valid", "namespace"), "key")) is None
+
+
+async def test_async_batch_store_deduplication(mocker: MockerFixture) -> None:
+ abatch = mocker.spy(InMemoryStore, "batch")
+ store = MockAsyncBatchedStore()
+
+ same_doc = {"value": "same"}
+ diff_doc = {"value": "different"}
+ await asyncio.gather(
+ store.aput(namespace=("test",), key="same", value=same_doc),
+ store.aput(namespace=("test",), key="different", value=diff_doc),
+ )
+ abatch.reset_mock()
+
+ results = await asyncio.gather(
+ store.aget(namespace=("test",), key="same"),
+ store.aget(namespace=("test",), key="same"),
+ store.aget(namespace=("test",), key="different"),
+ )
+
+ assert len(results) == 3
+ assert results[0] == results[1]
+ assert results[0] != results[2]
+ assert results[0].value == same_doc # type: ignore
+ assert results[2].value == diff_doc # type: ignore
+ assert len(abatch.call_args_list) == 1
+ ops = list(abatch.call_args_list[0].args[1])
+ assert len(ops) == 2
+ assert GetOp(("test",), "same") in ops
+ assert GetOp(("test",), "different") in ops
+
+ abatch.reset_mock()
+
+ doc1 = {"value": 1}
+ doc2 = {"value": 2}
+ results = await asyncio.gather(
+ store.aput(namespace=("test",), key="key", value=doc1),
+ store.aput(namespace=("test",), key="key", value=doc2),
+ )
+ assert len(abatch.call_args_list) == 1
+ ops = list(abatch.call_args_list[0].args[1])
+ assert len(ops) == 1
+ assert ops[0] == PutOp(("test",), "key", doc2)
+ assert len(results) == 2
+ assert all(result is None for result in results)
+
+ result = await store.aget(namespace=("test",), key="key")
+ assert result is not None
+ assert result.value == doc2
+
+ abatch.reset_mock()
+
+ results = await asyncio.gather(
+ store.asearch(("test",), filter={"value": 2}),
+ store.asearch(("test",), filter={"value": 2}),
+ )
+ assert len(abatch.call_args_list) == 1
+ ops = list(abatch.call_args_list[0].args[1])
+ assert len(ops) == 1
+ assert len(results) == 2
+ assert results[0] == results[1]
+ assert len(results[0]) == 1
+ assert results[0][0].value == doc2
+
+ abatch.reset_mock()
From 8f649abd0a0beae0727959b115e2f8efd181c6de Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Mon, 25 Nov 2024 17:44:45 -0800
Subject: [PATCH 047/149] Release PG Checkpointer (#2536)
---
libs/checkpoint-postgres/pyproject.toml | 2 +-
libs/checkpoint/pyproject.toml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/libs/checkpoint-postgres/pyproject.toml b/libs/checkpoint-postgres/pyproject.toml
index 90a68eabf..b879a3a6e 100644
--- a/libs/checkpoint-postgres/pyproject.toml
+++ b/libs/checkpoint-postgres/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-postgres"
-version = "2.0.3"
+version = "2.0.4"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
diff --git a/libs/checkpoint/pyproject.toml b/libs/checkpoint/pyproject.toml
index deb7de5c4..278594fcb 100644
--- a/libs/checkpoint/pyproject.toml
+++ b/libs/checkpoint/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
-version = "2.0.5"
+version = "2.0.6"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
From f04ce5d1ee65e25b59014b182b7cb4adb47065ff Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Tue, 26 Nov 2024 07:56:57 -0800
Subject: [PATCH 048/149] [CLI] Add python-dotenv for inmem group (#2540)
---
libs/cli/poetry.lock | 539 +++++++++++++++++++++-------------------
libs/cli/pyproject.toml | 9 +-
2 files changed, 284 insertions(+), 264 deletions(-)
diff --git a/libs/cli/poetry.lock b/libs/cli/poetry.lock
index 5f88d1e84..29045a438 100644
--- a/libs/cli/poetry.lock
+++ b/libs/cli/poetry.lock
@@ -526,13 +526,13 @@ tests = ["flask (>=2.2.5)", "hypothesis (>=6.79.4)", "pytest (>=7.4.4)"]
[[package]]
name = "langchain-core"
-version = "0.3.19"
+version = "0.3.21"
description = "Building applications with LLMs through composability"
optional = true
python-versions = "<4.0,>=3.9"
files = [
- {file = "langchain_core-0.3.19-py3-none-any.whl", hash = "sha256:562b7cc3c15dfaa9270cb1496990c1f3b3e0b660c4d6a3236d7f693346f2a96c"},
- {file = "langchain_core-0.3.19.tar.gz", hash = "sha256:126d9e8cadb2a5b8d1793a228c0783a3b608e36064d5a2ef1a4d38d07a344523"},
+ {file = "langchain_core-0.3.21-py3-none-any.whl", hash = "sha256:7e723dff80946a1198976c6876fea8326dc82566ef9bcb5f8d9188f738733665"},
+ {file = "langchain_core-0.3.21.tar.gz", hash = "sha256:561b52b258ffa50a9fb11d7a1940ebfd915654d1ec95b35e81dfd5ee84143411"},
]
[package.dependencies]
@@ -593,13 +593,13 @@ watchfiles = ">=0.13"
[[package]]
name = "langgraph-checkpoint"
-version = "2.0.5"
+version = "2.0.6"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = true
python-versions = "<4.0.0,>=3.9.0"
files = [
- {file = "langgraph_checkpoint-2.0.5-py3-none-any.whl", hash = "sha256:0e7e730ea9358577bdcdeb6a17d8f340bad59770e2895a8a7fc853a76e08400b"},
- {file = "langgraph_checkpoint-2.0.5.tar.gz", hash = "sha256:48612cdaf98c40a998079d222abb196a61e504d04dea65c7820d738d42150cac"},
+ {file = "langgraph_checkpoint-2.0.6-py3-none-any.whl", hash = "sha256:2878283c3ee2519bf180df9b7b7155b73fa05eb63b1af9600a03e03a930d8c53"},
+ {file = "langgraph_checkpoint-2.0.6.tar.gz", hash = "sha256:69ab9c61c4e2992264671f55579c24070b7b6cedc105a33da3fba6526df248cf"},
]
[package.dependencies]
@@ -624,13 +624,13 @@ orjson = ">=3.10.1"
[[package]]
name = "langsmith"
-version = "0.1.144"
+version = "0.1.146"
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
optional = true
python-versions = "<4.0,>=3.8.1"
files = [
- {file = "langsmith-0.1.144-py3-none-any.whl", hash = "sha256:08ffb975bff2e82fc6f5428837c64c074ea25102d08a25e256361a80812c6100"},
- {file = "langsmith-0.1.144.tar.gz", hash = "sha256:b621f358d5a33441d7b5e7264c376bf4ea82bfc62d7e41aafc0f8094e3bd6369"},
+ {file = "langsmith-0.1.146-py3-none-any.whl", hash = "sha256:9d062222f1a32c9b047dab0149b24958f988989cd8d4a5f9139ff959a51e59d8"},
+ {file = "langsmith-0.1.146.tar.gz", hash = "sha256:ead8b0b9d5b6cd3ac42937ec48bdf09d4afe7ca1bba22dc05eb65591a18106f8"},
]
[package.dependencies]
@@ -782,69 +782,86 @@ files = [
[[package]]
name = "orjson"
-version = "3.10.11"
+version = "3.10.12"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = true
python-versions = ">=3.8"
files = [
- {file = "orjson-3.10.11-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6dade64687f2bd7c090281652fe18f1151292d567a9302b34c2dbb92a3872f1f"},
- {file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82f07c550a6ccd2b9290849b22316a609023ed851a87ea888c0456485a7d196a"},
- {file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd9a187742d3ead9df2e49240234d728c67c356516cf4db018833a86f20ec18c"},
- {file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77b0fed6f209d76c1c39f032a70df2d7acf24b1812ca3e6078fd04e8972685a3"},
- {file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63fc9d5fe1d4e8868f6aae547a7b8ba0a2e592929245fff61d633f4caccdcdd6"},
- {file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65cd3e3bb4fbb4eddc3c1e8dce10dc0b73e808fcb875f9fab40c81903dd9323e"},
- {file = "orjson-3.10.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f67c570602300c4befbda12d153113b8974a3340fdcf3d6de095ede86c06d92"},
- {file = "orjson-3.10.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1f39728c7f7d766f1f5a769ce4d54b5aaa4c3f92d5b84817053cc9995b977acc"},
- {file = "orjson-3.10.11-cp310-none-win32.whl", hash = "sha256:1789d9db7968d805f3d94aae2c25d04014aae3a2fa65b1443117cd462c6da647"},
- {file = "orjson-3.10.11-cp310-none-win_amd64.whl", hash = "sha256:5576b1e5a53a5ba8f8df81872bb0878a112b3ebb1d392155f00f54dd86c83ff6"},
- {file = "orjson-3.10.11-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1444f9cb7c14055d595de1036f74ecd6ce15f04a715e73f33bb6326c9cef01b6"},
- {file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdec57fe3b4bdebcc08a946db3365630332dbe575125ff3d80a3272ebd0ddafe"},
- {file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eed32f33a0ea6ef36ccc1d37f8d17f28a1d6e8eefae5928f76aff8f1df85e67"},
- {file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80df27dd8697242b904f4ea54820e2d98d3f51f91e97e358fc13359721233e4b"},
- {file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:705f03cee0cb797256d54de6695ef219e5bc8c8120b6654dd460848d57a9af3d"},
- {file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03246774131701de8e7059b2e382597da43144a9a7400f178b2a32feafc54bd5"},
- {file = "orjson-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8b5759063a6c940a69c728ea70d7c33583991c6982915a839c8da5f957e0103a"},
- {file = "orjson-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:677f23e32491520eebb19c99bb34675daf5410c449c13416f7f0d93e2cf5f981"},
- {file = "orjson-3.10.11-cp311-none-win32.whl", hash = "sha256:a11225d7b30468dcb099498296ffac36b4673a8398ca30fdaec1e6c20df6aa55"},
- {file = "orjson-3.10.11-cp311-none-win_amd64.whl", hash = "sha256:df8c677df2f9f385fcc85ab859704045fa88d4668bc9991a527c86e710392bec"},
- {file = "orjson-3.10.11-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:360a4e2c0943da7c21505e47cf6bd725588962ff1d739b99b14e2f7f3545ba51"},
- {file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:496e2cb45de21c369079ef2d662670a4892c81573bcc143c4205cae98282ba97"},
- {file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7dfa8db55c9792d53c5952900c6a919cfa377b4f4534c7a786484a6a4a350c19"},
- {file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51f3382415747e0dbda9dade6f1e1a01a9d37f630d8c9049a8ed0e385b7a90c0"},
- {file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f35a1b9f50a219f470e0e497ca30b285c9f34948d3c8160d5ad3a755d9299433"},
- {file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f3b7c5803138e67028dde33450e054c87e0703afbe730c105f1fcd873496d5"},
- {file = "orjson-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f91d9eb554310472bd09f5347950b24442600594c2edc1421403d7610a0998fd"},
- {file = "orjson-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dfbb2d460a855c9744bbc8e36f9c3a997c4b27d842f3d5559ed54326e6911f9b"},
- {file = "orjson-3.10.11-cp312-none-win32.whl", hash = "sha256:d4a62c49c506d4d73f59514986cadebb7e8d186ad510c518f439176cf8d5359d"},
- {file = "orjson-3.10.11-cp312-none-win_amd64.whl", hash = "sha256:f1eec3421a558ff7a9b010a6c7effcfa0ade65327a71bb9b02a1c3b77a247284"},
- {file = "orjson-3.10.11-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c46294faa4e4d0eb73ab68f1a794d2cbf7bab33b1dda2ac2959ffb7c61591899"},
- {file = "orjson-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52e5834d7d6e58a36846e059d00559cb9ed20410664f3ad156cd2cc239a11230"},
- {file = "orjson-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2fc947e5350fdce548bfc94f434e8760d5cafa97fb9c495d2fef6757aa02ec0"},
- {file = "orjson-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0efabbf839388a1dab5b72b5d3baedbd6039ac83f3b55736eb9934ea5494d258"},
- {file = "orjson-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3f29634260708c200c4fe148e42b4aae97d7b9fee417fbdd74f8cfc265f15b0"},
- {file = "orjson-3.10.11-cp313-none-win32.whl", hash = "sha256:1a1222ffcee8a09476bbdd5d4f6f33d06d0d6642df2a3d78b7a195ca880d669b"},
- {file = "orjson-3.10.11-cp313-none-win_amd64.whl", hash = "sha256:bc274ac261cc69260913b2d1610760e55d3c0801bb3457ba7b9004420b6b4270"},
- {file = "orjson-3.10.11-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:19b3763e8bbf8ad797df6b6b5e0fc7c843ec2e2fc0621398534e0c6400098f87"},
- {file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be83a13312e5e58d633580c5eb8d0495ae61f180da2722f20562974188af205"},
- {file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:afacfd1ab81f46dedd7f6001b6d4e8de23396e4884cd3c3436bd05defb1a6446"},
- {file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb4d0bea56bba596723d73f074c420aec3b2e5d7d30698bc56e6048066bd560c"},
- {file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96ed1de70fcb15d5fed529a656df29f768187628727ee2788344e8a51e1c1350"},
- {file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bfb30c891b530f3f80e801e3ad82ef150b964e5c38e1fb8482441c69c35c61c"},
- {file = "orjson-3.10.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d496c74fc2b61341e3cefda7eec21b7854c5f672ee350bc55d9a4997a8a95204"},
- {file = "orjson-3.10.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:655a493bac606655db9a47fe94d3d84fc7f3ad766d894197c94ccf0c5408e7d3"},
- {file = "orjson-3.10.11-cp38-none-win32.whl", hash = "sha256:b9546b278c9fb5d45380f4809e11b4dd9844ca7aaf1134024503e134ed226161"},
- {file = "orjson-3.10.11-cp38-none-win_amd64.whl", hash = "sha256:b592597fe551d518f42c5a2eb07422eb475aa8cfdc8c51e6da7054b836b26782"},
- {file = "orjson-3.10.11-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c95f2ecafe709b4e5c733b5e2768ac569bed308623c85806c395d9cca00e08af"},
- {file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80c00d4acded0c51c98754fe8218cb49cb854f0f7eb39ea4641b7f71732d2cb7"},
- {file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:461311b693d3d0a060439aa669c74f3603264d4e7a08faa68c47ae5a863f352d"},
- {file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52ca832f17d86a78cbab86cdc25f8c13756ebe182b6fc1a97d534051c18a08de"},
- {file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c57ea78a753812f528178aa2f1c57da633754c91d2124cb28991dab4c79a54"},
- {file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7fcfc6f7ca046383fb954ba528587e0f9336828b568282b27579c49f8e16aad"},
- {file = "orjson-3.10.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:86b9dd983857970c29e4c71bb3e95ff085c07d3e83e7c46ebe959bac07ebd80b"},
- {file = "orjson-3.10.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4d83f87582d223e54efb2242a79547611ba4ebae3af8bae1e80fa9a0af83bb7f"},
- {file = "orjson-3.10.11-cp39-none-win32.whl", hash = "sha256:9fd0ad1c129bc9beb1154c2655f177620b5beaf9a11e0d10bac63ef3fce96950"},
- {file = "orjson-3.10.11-cp39-none-win_amd64.whl", hash = "sha256:10f416b2a017c8bd17f325fb9dee1fb5cdd7a54e814284896b7c3f2763faa017"},
- {file = "orjson-3.10.11.tar.gz", hash = "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef"},
+ {file = "orjson-3.10.12-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ece01a7ec71d9940cc654c482907a6b65df27251255097629d0dea781f255c6d"},
+ {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34ec9aebc04f11f4b978dd6caf697a2df2dd9b47d35aa4cc606cabcb9df69d7"},
+ {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd6ec8658da3480939c79b9e9e27e0db31dffcd4ba69c334e98c9976ac29140e"},
+ {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17e6baf4cf01534c9de8a16c0c611f3d94925d1701bf5f4aff17003677d8ced"},
+ {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6402ebb74a14ef96f94a868569f5dccf70d791de49feb73180eb3c6fda2ade56"},
+ {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0000758ae7c7853e0a4a6063f534c61656ebff644391e1f81698c1b2d2fc8cd2"},
+ {file = "orjson-3.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:888442dcee99fd1e5bd37a4abb94930915ca6af4db50e23e746cdf4d1e63db13"},
+ {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c1f7a3ce79246aa0e92f5458d86c54f257fb5dfdc14a192651ba7ec2c00f8a05"},
+ {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:802a3935f45605c66fb4a586488a38af63cb37aaad1c1d94c982c40dcc452e85"},
+ {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1da1ef0113a2be19bb6c557fb0ec2d79c92ebd2fed4cfb1b26bab93f021fb885"},
+ {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a3273e99f367f137d5b3fecb5e9f45bcdbfac2a8b2f32fbc72129bbd48789c2"},
+ {file = "orjson-3.10.12-cp310-none-win32.whl", hash = "sha256:475661bf249fd7907d9b0a2a2421b4e684355a77ceef85b8352439a9163418c3"},
+ {file = "orjson-3.10.12-cp310-none-win_amd64.whl", hash = "sha256:87251dc1fb2b9e5ab91ce65d8f4caf21910d99ba8fb24b49fd0c118b2362d509"},
+ {file = "orjson-3.10.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a734c62efa42e7df94926d70fe7d37621c783dea9f707a98cdea796964d4cf74"},
+ {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:750f8b27259d3409eda8350c2919a58b0cfcd2054ddc1bd317a643afc646ef23"},
+ {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb52c22bfffe2857e7aa13b4622afd0dd9d16ea7cc65fd2bf318d3223b1b6252"},
+ {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:440d9a337ac8c199ff8251e100c62e9488924c92852362cd27af0e67308c16ef"},
+ {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e15c06491c69997dfa067369baab3bf094ecb74be9912bdc4339972323f252"},
+ {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:362d204ad4b0b8724cf370d0cd917bb2dc913c394030da748a3bb632445ce7c4"},
+ {file = "orjson-3.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b57cbb4031153db37b41622eac67329c7810e5f480fda4cfd30542186f006ae"},
+ {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:165c89b53ef03ce0d7c59ca5c82fa65fe13ddf52eeb22e859e58c237d4e33b9b"},
+ {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5dee91b8dfd54557c1a1596eb90bcd47dbcd26b0baaed919e6861f076583e9da"},
+ {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a4e1cfb72de6f905bdff061172adfb3caf7a4578ebf481d8f0530879476c07"},
+ {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:038d42c7bc0606443459b8fe2d1f121db474c49067d8d14c6a075bbea8bf14dd"},
+ {file = "orjson-3.10.12-cp311-none-win32.whl", hash = "sha256:03b553c02ab39bed249bedd4abe37b2118324d1674e639b33fab3d1dafdf4d79"},
+ {file = "orjson-3.10.12-cp311-none-win_amd64.whl", hash = "sha256:8b8713b9e46a45b2af6b96f559bfb13b1e02006f4242c156cbadef27800a55a8"},
+ {file = "orjson-3.10.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53206d72eb656ca5ac7d3a7141e83c5bbd3ac30d5eccfe019409177a57634b0d"},
+ {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8010afc2150d417ebda810e8df08dd3f544e0dd2acab5370cfa6bcc0662f8f"},
+ {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed459b46012ae950dd2e17150e838ab08215421487371fa79d0eced8d1461d70"},
+ {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dcb9673f108a93c1b52bfc51b0af422c2d08d4fc710ce9c839faad25020bb69"},
+ {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22a51ae77680c5c4652ebc63a83d5255ac7d65582891d9424b566fb3b5375ee9"},
+ {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910fdf2ac0637b9a77d1aad65f803bac414f0b06f720073438a7bd8906298192"},
+ {file = "orjson-3.10.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24ce85f7100160936bc2116c09d1a8492639418633119a2224114f67f63a4559"},
+ {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a76ba5fc8dd9c913640292df27bff80a685bed3a3c990d59aa6ce24c352f8fc"},
+ {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ff70ef093895fd53f4055ca75f93f047e088d1430888ca1229393a7c0521100f"},
+ {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f4244b7018b5753ecd10a6d324ec1f347da130c953a9c88432c7fbc8875d13be"},
+ {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16135ccca03445f37921fa4b585cff9a58aa8d81ebcb27622e69bfadd220b32c"},
+ {file = "orjson-3.10.12-cp312-none-win32.whl", hash = "sha256:2d879c81172d583e34153d524fcba5d4adafbab8349a7b9f16ae511c2cee8708"},
+ {file = "orjson-3.10.12-cp312-none-win_amd64.whl", hash = "sha256:fc23f691fa0f5c140576b8c365bc942d577d861a9ee1142e4db468e4e17094fb"},
+ {file = "orjson-3.10.12-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47962841b2a8aa9a258b377f5188db31ba49af47d4003a32f55d6f8b19006543"},
+ {file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6334730e2532e77b6054e87ca84f3072bee308a45a452ea0bffbbbc40a67e296"},
+ {file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:accfe93f42713c899fdac2747e8d0d5c659592df2792888c6c5f829472e4f85e"},
+ {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7974c490c014c48810d1dede6c754c3cc46598da758c25ca3b4001ac45b703f"},
+ {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3f250ce7727b0b2682f834a3facff88e310f52f07a5dcfd852d99637d386e79e"},
+ {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f31422ff9486ae484f10ffc51b5ab2a60359e92d0716fcce1b3593d7bb8a9af6"},
+ {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5f29c5d282bb2d577c2a6bbde88d8fdcc4919c593f806aac50133f01b733846e"},
+ {file = "orjson-3.10.12-cp313-none-win32.whl", hash = "sha256:f45653775f38f63dc0e6cd4f14323984c3149c05d6007b58cb154dd080ddc0dc"},
+ {file = "orjson-3.10.12-cp313-none-win_amd64.whl", hash = "sha256:229994d0c376d5bdc91d92b3c9e6be2f1fbabd4cc1b59daae1443a46ee5e9825"},
+ {file = "orjson-3.10.12-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7d69af5b54617a5fac5c8e5ed0859eb798e2ce8913262eb522590239db6c6763"},
+ {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ed119ea7d2953365724a7059231a44830eb6bbb0cfead33fcbc562f5fd8f935"},
+ {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5fc1238ef197e7cad5c91415f524aaa51e004be5a9b35a1b8a84ade196f73f"},
+ {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43509843990439b05f848539d6f6198d4ac86ff01dd024b2f9a795c0daeeab60"},
+ {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f72e27a62041cfb37a3de512247ece9f240a561e6c8662276beaf4d53d406db4"},
+ {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a904f9572092bb6742ab7c16c623f0cdccbad9eeb2d14d4aa06284867bddd31"},
+ {file = "orjson-3.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:855c0833999ed5dc62f64552db26f9be767434917d8348d77bacaab84f787d7b"},
+ {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:897830244e2320f6184699f598df7fb9db9f5087d6f3f03666ae89d607e4f8ed"},
+ {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:0b32652eaa4a7539f6f04abc6243619c56f8530c53bf9b023e1269df5f7816dd"},
+ {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:36b4aa31e0f6a1aeeb6f8377769ca5d125db000f05c20e54163aef1d3fe8e833"},
+ {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5535163054d6cbf2796f93e4f0dbc800f61914c0e3c4ed8499cf6ece22b4a3da"},
+ {file = "orjson-3.10.12-cp38-none-win32.whl", hash = "sha256:90a5551f6f5a5fa07010bf3d0b4ca2de21adafbbc0af6cb700b63cd767266cb9"},
+ {file = "orjson-3.10.12-cp38-none-win_amd64.whl", hash = "sha256:703a2fb35a06cdd45adf5d733cf613cbc0cb3ae57643472b16bc22d325b5fb6c"},
+ {file = "orjson-3.10.12-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f29de3ef71a42a5822765def1febfb36e0859d33abf5c2ad240acad5c6a1b78d"},
+ {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de365a42acc65d74953f05e4772c974dad6c51cfc13c3240899f534d611be967"},
+ {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a5a0158648a67ff0004cb0df5df7dcc55bfc9ca154d9c01597a23ad54c8d0c"},
+ {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c47ce6b8d90fe9646a25b6fb52284a14ff215c9595914af63a5933a49972ce36"},
+ {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0eee4c2c5bfb5c1b47a5db80d2ac7aaa7e938956ae88089f098aff2c0f35d5d8"},
+ {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d3081bbe8b86587eb5c98a73b97f13d8f9fea685cf91a579beddacc0d10566"},
+ {file = "orjson-3.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c23a6e90383884068bc2dba83d5222c9fcc3b99a0ed2411d38150734236755"},
+ {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5472be7dc3269b4b52acba1433dac239215366f89dc1d8d0e64029abac4e714e"},
+ {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7319cda750fca96ae5973efb31b17d97a5c5225ae0bc79bf5bf84df9e1ec2ab6"},
+ {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:74d5ca5a255bf20b8def6a2b96b1e18ad37b4a122d59b154c458ee9494377f80"},
+ {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ff31d22ecc5fb85ef62c7d4afe8301d10c558d00dd24274d4bbe464380d3cd69"},
+ {file = "orjson-3.10.12-cp39-none-win32.whl", hash = "sha256:c22c3ea6fba91d84fcb4cda30e64aff548fcf0c44c876e681f47d61d24b12e6b"},
+ {file = "orjson-3.10.12-cp39-none-win_amd64.whl", hash = "sha256:be604f60d45ace6b0b33dd990a66b4526f1a7a186ac411c942674625456ca548"},
+ {file = "orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff"},
]
[[package]]
@@ -886,18 +903,18 @@ files = [
[[package]]
name = "pydantic"
-version = "2.10.0"
+version = "2.10.2"
description = "Data validation using Python type hints"
optional = true
python-versions = ">=3.8"
files = [
- {file = "pydantic-2.10.0-py3-none-any.whl", hash = "sha256:5e7807ba9201bdf61b1b58aa6eb690916c40a47acfb114b1b4fef3e7fd5b30fc"},
- {file = "pydantic-2.10.0.tar.gz", hash = "sha256:0aca0f045ff6e2f097f1fe89521115335f15049eeb8a7bef3dafe4b19a74e289"},
+ {file = "pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e"},
+ {file = "pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
-pydantic-core = "2.27.0"
+pydantic-core = "2.27.1"
typing-extensions = ">=4.12.2"
[package.extras]
@@ -906,111 +923,111 @@ timezone = ["tzdata"]
[[package]]
name = "pydantic-core"
-version = "2.27.0"
+version = "2.27.1"
description = "Core functionality for Pydantic validation and serialization"
optional = true
python-versions = ">=3.8"
files = [
- {file = "pydantic_core-2.27.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2ac6b919f7fed71b17fe0b4603c092a4c9b5bae414817c9c81d3c22d1e1bcc"},
- {file = "pydantic_core-2.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e015833384ca3e1a0565a79f5d953b0629d9138021c27ad37c92a9fa1af7623c"},
- {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db72e40628967f6dc572020d04b5f800d71264e0531c6da35097e73bdf38b003"},
- {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df45c4073bed486ea2f18757057953afed8dd77add7276ff01bccb79982cf46c"},
- {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:836a4bfe0cc6d36dc9a9cc1a7b391265bf6ce9d1eb1eac62ac5139f5d8d9a6fa"},
- {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bf1340ae507f6da6360b24179c2083857c8ca7644aab65807023cf35404ea8d"},
- {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ab325fc86fbc077284c8d7f996d904d30e97904a87d6fb303dce6b3de7ebba9"},
- {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1da0c98a85a6c6ed702d5556db3b09c91f9b0b78de37b7593e2de8d03238807a"},
- {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7b0202ebf2268954090209a84f9897345719e46a57c5f2c9b7b250ca0a9d3e63"},
- {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:35380671c3c921fe8adf31ad349dc6f7588b7e928dbe44e1093789734f607399"},
- {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b4c19525c3538fbc0bbda6229f9682fb8199ce9ac37395880e6952798e00373"},
- {file = "pydantic_core-2.27.0-cp310-none-win32.whl", hash = "sha256:333c840a1303d1474f491e7be0b718226c730a39ead0f7dab2c7e6a2f3855555"},
- {file = "pydantic_core-2.27.0-cp310-none-win_amd64.whl", hash = "sha256:99b2863c1365f43f74199c980a3d40f18a218fbe683dd64e470199db426c4d6a"},
- {file = "pydantic_core-2.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4523c4009c3f39d948e01962223c9f5538602e7087a628479b723c939fab262d"},
- {file = "pydantic_core-2.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84af1cf7bfdcbc6fcf5a5f70cc9896205e0350306e4dd73d54b6a18894f79386"},
- {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e65466b31be1070b4a5b7dbfbd14b247884cb8e8b79c64fb0f36b472912dbaea"},
- {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a5c022bb0d453192426221605efc865373dde43b17822a264671c53b068ac20c"},
- {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bb69bf3b6500f195c3deb69c1205ba8fc3cb21d1915f1f158a10d6b1ef29b6a"},
- {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aa4d1b2eba9a325897308b3124014a142cdccb9f3e016f31d3ebee6b5ea5e75"},
- {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e96ca781e0c01e32115912ebdf7b3fb0780ce748b80d7d28a0802fa9fbaf44e"},
- {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b872c86d8d71827235c7077461c502feb2db3f87d9d6d5a9daa64287d75e4fa0"},
- {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:82e1ad4ca170e8af4c928b67cff731b6296e6a0a0981b97b2eb7c275cc4e15bd"},
- {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:eb40f828bc2f73f777d1eb8fee2e86cd9692a4518b63b6b5aa8af915dfd3207b"},
- {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9a8fbf506fde1529a1e3698198fe64bfbe2e0c09557bc6a7dcf872e7c01fec40"},
- {file = "pydantic_core-2.27.0-cp311-none-win32.whl", hash = "sha256:24f984fc7762ed5f806d9e8c4c77ea69fdb2afd987b4fd319ef06c87595a8c55"},
- {file = "pydantic_core-2.27.0-cp311-none-win_amd64.whl", hash = "sha256:68950bc08f9735306322bfc16a18391fcaac99ded2509e1cc41d03ccb6013cfe"},
- {file = "pydantic_core-2.27.0-cp311-none-win_arm64.whl", hash = "sha256:3eb8849445c26b41c5a474061032c53e14fe92a11a5db969f722a2716cd12206"},
- {file = "pydantic_core-2.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8117839a9bdbba86e7f9df57018fe3b96cec934c3940b591b0fd3fbfb485864a"},
- {file = "pydantic_core-2.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a291d0b4243a259c8ea7e2b84eb9ccb76370e569298875a7c5e3e71baf49057a"},
- {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e35afd9e10b2698e6f2f32256678cb23ca6c1568d02628033a837638b3ed12"},
- {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58ab0d979c969983cdb97374698d847a4acffb217d543e172838864636ef10d9"},
- {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d06b667e53320332be2bf6f9461f4a9b78092a079b8ce8634c9afaa7e10cd9f"},
- {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78f841523729e43e3928a364ec46e2e3f80e6625a4f62aca5c345f3f626c6e8a"},
- {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:400bf470e4327e920883b51e255617dfe4496d4e80c3fea0b5a5d0bf2c404dd4"},
- {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:951e71da6c89d354572098bada5ba5b5dc3a9390c933af8a614e37755d3d1840"},
- {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a51ce96224eadd1845150b204389623c8e129fde5a67a84b972bd83a85c6c40"},
- {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:483c2213a609e7db2c592bbc015da58b6c75af7360ca3c981f178110d9787bcf"},
- {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:359e7951f04ad35111b5ddce184db3391442345d0ab073aa63a95eb8af25a5ef"},
- {file = "pydantic_core-2.27.0-cp312-none-win32.whl", hash = "sha256:ee7d9d5537daf6d5c74a83b38a638cc001b648096c1cae8ef695b0c919d9d379"},
- {file = "pydantic_core-2.27.0-cp312-none-win_amd64.whl", hash = "sha256:2be0ad541bb9f059954ccf8877a49ed73877f862529575ff3d54bf4223e4dd61"},
- {file = "pydantic_core-2.27.0-cp312-none-win_arm64.whl", hash = "sha256:6e19401742ed7b69e51d8e4df3c03ad5ec65a83b36244479fd70edde2828a5d9"},
- {file = "pydantic_core-2.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5f2b19b8d6fca432cb3acf48cf5243a7bf512988029b6e6fd27e9e8c0a204d85"},
- {file = "pydantic_core-2.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c86679f443e7085ea55a7376462553996c688395d18ef3f0d3dbad7838f857a2"},
- {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:510b11e9c3b1a852876d1ccd8d5903684336d635214148637ceb27366c75a467"},
- {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb704155e73b833801c247f39d562229c0303f54770ca14fb1c053acb376cf10"},
- {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ce048deb1e033e7a865ca384770bccc11d44179cf09e5193a535c4c2f497bdc"},
- {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58560828ee0951bb125c6f2862fbc37f039996d19ceb6d8ff1905abf7da0bf3d"},
- {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb4785894936d7682635726613c44578c420a096729f1978cd061a7e72d5275"},
- {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2883b260f7a93235488699d39cbbd94fa7b175d3a8063fbfddd3e81ad9988cb2"},
- {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c6fcb3fa3855d583aa57b94cf146f7781d5d5bc06cb95cb3afece33d31aac39b"},
- {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e851a051f7260e6d688267eb039c81f05f23a19431bd7dfa4bf5e3cb34c108cd"},
- {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edb1bfd45227dec8d50bc7c7d86463cd8728bcc574f9b07de7369880de4626a3"},
- {file = "pydantic_core-2.27.0-cp313-none-win32.whl", hash = "sha256:678f66462058dd978702db17eb6a3633d634f7aa0deaea61e0a674152766d3fc"},
- {file = "pydantic_core-2.27.0-cp313-none-win_amd64.whl", hash = "sha256:d28ca7066d6cdd347a50d8b725dc10d9a1d6a1cce09836cf071ea6a2d4908be0"},
- {file = "pydantic_core-2.27.0-cp313-none-win_arm64.whl", hash = "sha256:6f4a53af9e81d757756508b57cae1cf28293f0f31b9fa2bfcb416cc7fb230f9d"},
- {file = "pydantic_core-2.27.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:e9f9feee7f334b72ceae46313333d002b56f325b5f04271b4ae2aadd9e993ae4"},
- {file = "pydantic_core-2.27.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:225bfff5d425c34e1fd562cef52d673579d59b967d9de06178850c4802af9039"},
- {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921ad596ff1a82f9c692b0758c944355abc9f0de97a4c13ca60ffc6d8dc15d4"},
- {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6354e18a9be37bfa124d6b288a87fb30c673745806c92956f1a25e3ae6e76b96"},
- {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ee4c2a75af9fe21269a4a0898c5425afb01af1f5d276063f57e2ae1bc64e191"},
- {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c91e3c04f5191fd3fb68764bddeaf02025492d5d9f23343b283870f6ace69708"},
- {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6ebfac28fd51890a61df36ef202adbd77d00ee5aca4a3dadb3d9ed49cfb929"},
- {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36aa167f69d8807ba7e341d67ea93e50fcaaf6bc433bb04939430fa3dab06f31"},
- {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e8d89c276234579cd3d095d5fa2a44eb10db9a218664a17b56363cddf226ff3"},
- {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:5cc822ab90a70ea3a91e6aed3afac570b276b1278c6909b1d384f745bd09c714"},
- {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e15315691fe2253eb447503153acef4d7223dfe7e7702f9ed66539fcd0c43801"},
- {file = "pydantic_core-2.27.0-cp38-none-win32.whl", hash = "sha256:dfa5f5c0a4c8fced1422dc2ca7eefd872d5d13eb33cf324361dbf1dbfba0a9fe"},
- {file = "pydantic_core-2.27.0-cp38-none-win_amd64.whl", hash = "sha256:513cb14c0cc31a4dfd849a4674b20c46d87b364f997bbcb02282306f5e187abf"},
- {file = "pydantic_core-2.27.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:4148dc9184ab79e356dc00a4199dc0ee8647973332cb385fc29a7cced49b9f9c"},
- {file = "pydantic_core-2.27.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5fc72fbfebbf42c0856a824b8b0dc2b5cd2e4a896050281a21cfa6fed8879cb1"},
- {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:185ef205256cd8b38431205698531026979db89a79587725c1e55c59101d64e9"},
- {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:395e3e1148fa7809016231f8065f30bb0dc285a97b4dc4360cd86e17bab58af7"},
- {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33d14369739c5d07e2e7102cdb0081a1fa46ed03215e07f097b34e020b83b1ae"},
- {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7820bb0d65e3ce1e3e70b6708c2f66143f55912fa02f4b618d0f08b61575f12"},
- {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43b61989068de9ce62296cde02beffabcadb65672207fc51e7af76dca75e6636"},
- {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15e350efb67b855cd014c218716feea4986a149ed1f42a539edd271ee074a196"},
- {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:433689845288f9a1ee5714444e65957be26d30915f7745091ede4a83cfb2d7bb"},
- {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:3fd8bc2690e7c39eecdf9071b6a889ce7b22b72073863940edc2a0a23750ca90"},
- {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:884f1806609c2c66564082540cffc96868c5571c7c3cf3a783f63f2fb49bd3cd"},
- {file = "pydantic_core-2.27.0-cp39-none-win32.whl", hash = "sha256:bf37b72834e7239cf84d4a0b2c050e7f9e48bced97bad9bdf98d26b8eb72e846"},
- {file = "pydantic_core-2.27.0-cp39-none-win_amd64.whl", hash = "sha256:31a2cae5f059329f9cfe3d8d266d3da1543b60b60130d186d9b6a3c20a346361"},
- {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4fb49cfdb53af5041aba909be00cccfb2c0d0a2e09281bf542371c5fd36ad04c"},
- {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:49633583eb7dc5cba61aaf7cdb2e9e662323ad394e543ee77af265736bcd3eaa"},
- {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:153017e3d6cd3ce979de06d84343ca424bb6092727375eba1968c8b4693c6ecb"},
- {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff63a92f6e249514ef35bc795de10745be0226eaea06eb48b4bbeaa0c8850a4a"},
- {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5982048129f40b082c2654de10c0f37c67a14f5ff9d37cf35be028ae982f26df"},
- {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:91bc66f878557313c2a6bcf396e7befcffe5ab4354cfe4427318968af31143c3"},
- {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:68ef5377eb582fa4343c9d0b57a5b094046d447b4c73dd9fbd9ffb216f829e7d"},
- {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c5726eec789ee38f2c53b10b1821457b82274f81f4f746bb1e666d8741fcfadb"},
- {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c0c431e4be5c1a0c6654e0c31c661cd89e0ca956ef65305c3c3fd96f4e72ca39"},
- {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8e21d927469d04b39386255bf00d0feedead16f6253dcc85e9e10ddebc334084"},
- {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b51f964fcbb02949fc546022e56cdb16cda457af485e9a3e8b78ac2ecf5d77e"},
- {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a7fd4de38f7ff99a37e18fa0098c3140286451bc823d1746ba80cec5b433a1"},
- {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fda87808429c520a002a85d6e7cdadbf58231d60e96260976c5b8f9a12a8e13"},
- {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a150392102c402c538190730fda06f3bce654fc498865579a9f2c1d2b425833"},
- {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c9ed88b398ba7e3bad7bd64d66cc01dcde9cfcb7ec629a6fd78a82fa0b559d78"},
- {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:9fe94d9d2a2b4edd7a4b22adcd45814b1b59b03feb00e56deb2e89747aec7bfe"},
- {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d8b5ee4ae9170e2775d495b81f414cc20268041c42571530513496ba61e94ba3"},
- {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d29e235ce13c91902ef3efc3d883a677655b3908b1cbc73dee816e5e1f8f7739"},
- {file = "pydantic_core-2.27.0.tar.gz", hash = "sha256:f57783fbaf648205ac50ae7d646f27582fc706be3977e87c3c124e7a92407b10"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"},
+ {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"},
+ {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"},
+ {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"},
+ {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"},
+ {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"},
+ {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"},
+ {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"},
+ {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"},
+ {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"},
+ {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"},
+ {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"},
+ {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"},
+ {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"},
+ {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"},
+ {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"},
+ {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"},
]
[package.dependencies]
@@ -1106,6 +1123,20 @@ docopt = ">=0.4.0"
pytest = ">=2.6.4"
watchdog = ">=0.6.0"
+[[package]]
+name = "python-dotenv"
+version = "1.0.1"
+description = "Read key-value pairs from a .env file and set them as environment variables"
+optional = true
+python-versions = ">=3.8"
+files = [
+ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
+ {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
+]
+
+[package.extras]
+cli = ["click (>=5.0)"]
+
[[package]]
name = "pyyaml"
version = "6.0.2"
@@ -1410,103 +1441,91 @@ watchmedo = ["PyYAML (>=3.10)"]
[[package]]
name = "watchfiles"
-version = "0.24.0"
+version = "1.0.0"
description = "Simple, modern and high performance file watching and code reload in python."
optional = true
-python-versions = ">=3.8"
+python-versions = ">=3.9"
files = [
- {file = "watchfiles-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0"},
- {file = "watchfiles-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c"},
- {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361"},
- {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3"},
- {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571"},
- {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd"},
- {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a"},
- {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e"},
- {file = "watchfiles-0.24.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c"},
- {file = "watchfiles-0.24.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188"},
- {file = "watchfiles-0.24.0-cp310-none-win32.whl", hash = "sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735"},
- {file = "watchfiles-0.24.0-cp310-none-win_amd64.whl", hash = "sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04"},
- {file = "watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428"},
- {file = "watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c"},
- {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43"},
- {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327"},
- {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5"},
- {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61"},
- {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15"},
- {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823"},
- {file = "watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab"},
- {file = "watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec"},
- {file = "watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d"},
- {file = "watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c"},
- {file = "watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633"},
- {file = "watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a"},
- {file = "watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370"},
- {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6"},
- {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b"},
- {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e"},
- {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea"},
- {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f"},
- {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234"},
- {file = "watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef"},
- {file = "watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968"},
- {file = "watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444"},
- {file = "watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896"},
- {file = "watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418"},
- {file = "watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48"},
- {file = "watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90"},
- {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94"},
- {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e"},
- {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827"},
- {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df"},
- {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab"},
- {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f"},
- {file = "watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b"},
- {file = "watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18"},
- {file = "watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07"},
- {file = "watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366"},
- {file = "watchfiles-0.24.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318"},
- {file = "watchfiles-0.24.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05"},
- {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c"},
- {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83"},
- {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c"},
- {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b"},
- {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b"},
- {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91"},
- {file = "watchfiles-0.24.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b"},
- {file = "watchfiles-0.24.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22"},
- {file = "watchfiles-0.24.0-cp38-none-win32.whl", hash = "sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1"},
- {file = "watchfiles-0.24.0-cp38-none-win_amd64.whl", hash = "sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1"},
- {file = "watchfiles-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886"},
- {file = "watchfiles-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f"},
- {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855"},
- {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b"},
- {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430"},
- {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3"},
- {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a"},
- {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9"},
- {file = "watchfiles-0.24.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca"},
- {file = "watchfiles-0.24.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e"},
- {file = "watchfiles-0.24.0-cp39-none-win32.whl", hash = "sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da"},
- {file = "watchfiles-0.24.0-cp39-none-win_amd64.whl", hash = "sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f"},
- {file = "watchfiles-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f"},
- {file = "watchfiles-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b"},
- {file = "watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4"},
- {file = "watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a"},
- {file = "watchfiles-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be"},
- {file = "watchfiles-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5"},
- {file = "watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777"},
- {file = "watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e"},
- {file = "watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1"},
+ {file = "watchfiles-1.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1d19df28f99d6a81730658fbeb3ade8565ff687f95acb59665f11502b441be5f"},
+ {file = "watchfiles-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28babb38cf2da8e170b706c4b84aa7e4528a6fa4f3ee55d7a0866456a1662041"},
+ {file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12ab123135b2f42517f04e720526d41448667ae8249e651385afb5cda31fedc0"},
+ {file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13a4f9ee0cd25682679eea5c14fc629e2eaa79aab74d963bc4e21f43b8ea1877"},
+ {file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e1d9284cc84de7855fcf83472e51d32daf6f6cecd094160192628bc3fee1b78"},
+ {file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ee5edc939f53466b329bbf2e58333a5461e6c7b50c980fa6117439e2c18b42d"},
+ {file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dccfc70480087567720e4e36ec381bba1ed68d7e5f368fe40c93b3b1eba0105"},
+ {file = "watchfiles-1.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83a6d33a9eda0af6a7470240d1af487807adc269704fe76a4972dd982d16236"},
+ {file = "watchfiles-1.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:905f69aad276639eff3893759a07d44ea99560e67a1cf46ff389cd62f88872a2"},
+ {file = "watchfiles-1.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09551237645d6bff3972592f2aa5424df9290e7a2e15d63c5f47c48cde585935"},
+ {file = "watchfiles-1.0.0-cp310-none-win32.whl", hash = "sha256:d2b39aa8edd9e5f56f99a2a2740a251dc58515398e9ed5a4b3e5ff2827060755"},
+ {file = "watchfiles-1.0.0-cp310-none-win_amd64.whl", hash = "sha256:2de52b499e1ab037f1a87cb8ebcb04a819bf087b1015a4cf6dcf8af3c2a2613e"},
+ {file = "watchfiles-1.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fbd0ab7a9943bbddb87cbc2bf2f09317e74c77dc55b1f5657f81d04666c25269"},
+ {file = "watchfiles-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:774ef36b16b7198669ce655d4f75b4c3d370e7f1cbdfb997fb10ee98717e2058"},
+ {file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b4fb98100267e6a5ebaff6aaa5d20aea20240584647470be39fe4823012ac96"},
+ {file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0fc3bf0effa2d8075b70badfdd7fb839d7aa9cea650d17886982840d71fdeabf"},
+ {file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:648e2b6db53eca6ef31245805cd528a16f56fa4cc15aeec97795eaf713c11435"},
+ {file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa13d604fcb9417ae5f2e3de676e66aa97427d888e83662ad205bed35a313176"},
+ {file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:936f362e7ff28311b16f0b97ec51e8f2cc451763a3264640c6ed40fb252d1ee4"},
+ {file = "watchfiles-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:245fab124b9faf58430da547512d91734858df13f2ddd48ecfa5e493455ffccb"},
+ {file = "watchfiles-1.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4ff9c7e84e8b644a8f985c42bcc81457240316f900fc72769aaedec9d088055a"},
+ {file = "watchfiles-1.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c9a8d8fd97defe935ef8dd53d562e68942ad65067cd1c54d6ed8a088b1d931d"},
+ {file = "watchfiles-1.0.0-cp311-none-win32.whl", hash = "sha256:a0abf173975eb9dd17bb14c191ee79999e650997cc644562f91df06060610e62"},
+ {file = "watchfiles-1.0.0-cp311-none-win_amd64.whl", hash = "sha256:2a825ba4b32c214e3855b536eb1a1f7b006511d8e64b8215aac06eb680642d84"},
+ {file = "watchfiles-1.0.0-cp311-none-win_arm64.whl", hash = "sha256:a5a7a06cfc65e34fd0a765a7623c5ba14707a0870703888e51d3d67107589817"},
+ {file = "watchfiles-1.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:28fb64b5843d94e2c2483f7b024a1280662a44409bedee8f2f51439767e2d107"},
+ {file = "watchfiles-1.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e3750434c83b61abb3163b49c64b04180b85b4dabb29a294513faec57f2ffdb7"},
+ {file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bedf84835069f51c7b026b3ca04e2e747ea8ed0a77c72006172c72d28c9f69fc"},
+ {file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90004553be36427c3d06ec75b804233f8f816374165d5225b93abd94ba6e7234"},
+ {file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b46e15c34d4e401e976d6949ad3a74d244600d5c4b88c827a3fdf18691a46359"},
+ {file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:487d15927f1b0bd24e7df921913399bb1ab94424c386bea8b267754d698f8f0e"},
+ {file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ff236d7a3f4b0a42f699a22fc374ba526bc55048a70cbb299661158e1bb5e1f"},
+ {file = "watchfiles-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c01446626574561756067f00b37e6b09c8622b0fc1e9fdbc7cbcea328d4e514"},
+ {file = "watchfiles-1.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b551c465a59596f3d08170bd7e1c532c7260dd90ed8135778038e13c5d48aa81"},
+ {file = "watchfiles-1.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1ed613ee107269f66c2df631ec0fc8efddacface85314d392a4131abe299f00"},
+ {file = "watchfiles-1.0.0-cp312-none-win32.whl", hash = "sha256:5f75cd42e7e2254117cf37ff0e68c5b3f36c14543756b2da621408349bd9ca7c"},
+ {file = "watchfiles-1.0.0-cp312-none-win_amd64.whl", hash = "sha256:cf517701a4a872417f4e02a136e929537743461f9ec6cdb8184d9a04f4843545"},
+ {file = "watchfiles-1.0.0-cp312-none-win_arm64.whl", hash = "sha256:8a2127cd68950787ee36753e6d401c8ea368f73beaeb8e54df5516a06d1ecd82"},
+ {file = "watchfiles-1.0.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:95de85c254f7fe8cbdf104731f7f87f7f73ae229493bebca3722583160e6b152"},
+ {file = "watchfiles-1.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:533a7cbfe700e09780bb31c06189e39c65f06c7f447326fee707fd02f9a6e945"},
+ {file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2218e78e2c6c07b1634a550095ac2a429026b2d5cbcd49a594f893f2bb8c936"},
+ {file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9122b8fdadc5b341315d255ab51d04893f417df4e6c1743b0aac8bf34e96e025"},
+ {file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9272fdbc0e9870dac3b505bce1466d386b4d8d6d2bacf405e603108d50446940"},
+ {file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a3b33c3aefe9067ebd87846806cd5fc0b017ab70d628aaff077ab9abf4d06b3"},
+ {file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc338ce9f8846543d428260fa0f9a716626963148edc937d71055d01d81e1525"},
+ {file = "watchfiles-1.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ac778a460ea22d63c7e6fb0bc0f5b16780ff0b128f7f06e57aaec63bd339285"},
+ {file = "watchfiles-1.0.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:53ae447f06f8f29f5ab40140f19abdab822387a7c426a369eb42184b021e97eb"},
+ {file = "watchfiles-1.0.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1f73c2147a453315d672c1ad907abe6d40324e34a185b51e15624bc793f93cc6"},
+ {file = "watchfiles-1.0.0-cp313-none-win32.whl", hash = "sha256:eba98901a2eab909dbd79681190b9049acc650f6111fde1845484a4450761e98"},
+ {file = "watchfiles-1.0.0-cp313-none-win_amd64.whl", hash = "sha256:d562a6114ddafb09c33246c6ace7effa71ca4b6a2324a47f4b09b6445ea78941"},
+ {file = "watchfiles-1.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3d94fd83ed54266d789f287472269c0def9120a2022674990bd24ad989ebd7a0"},
+ {file = "watchfiles-1.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48051d1c504448b2fcda71c5e6e3610ae45de6a0b8f5a43b961f250be4bdf5a8"},
+ {file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29cf884ad4285d23453c702ed03d689f9c0e865e3c85d20846d800d4787de00f"},
+ {file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d3572d4c34c4e9c33d25b3da47d9570d5122f8433b9ac6519dca49c2740d23cd"},
+ {file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c2696611182c85eb0e755b62b456f48debff484b7306b56f05478b843ca8ece"},
+ {file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:550109001920a993a4383b57229c717fa73627d2a4e8fcb7ed33c7f1cddb0c85"},
+ {file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b555a93c15bd2c71081922be746291d776d47521a00703163e5fbe6d2a402399"},
+ {file = "watchfiles-1.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:947ccba18a38b85c366dafeac8df2f6176342d5992ca240a9d62588b214d731f"},
+ {file = "watchfiles-1.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ffd98a299b0a74d1b704ef0ed959efb753e656a4e0425c14e46ae4c3cbdd2919"},
+ {file = "watchfiles-1.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f8c4f3a1210ed099a99e6a710df4ff2f8069411059ffe30fa5f9467ebed1256b"},
+ {file = "watchfiles-1.0.0-cp39-none-win32.whl", hash = "sha256:1e176b6b4119b3f369b2b4e003d53a226295ee862c0962e3afd5a1c15680b4e3"},
+ {file = "watchfiles-1.0.0-cp39-none-win_amd64.whl", hash = "sha256:2d9c0518fabf4a3f373b0a94bb9e4ea7a1df18dec45e26a4d182aa8918dee855"},
+ {file = "watchfiles-1.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f159ac795785cde4899e0afa539f4c723fb5dd336ce5605bc909d34edd00b79b"},
+ {file = "watchfiles-1.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c3d258d78341d5d54c0c804a5b7faa66cd30ba50b2756a7161db07ce15363b8d"},
+ {file = "watchfiles-1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bbd0311588c2de7f9ea5cf3922ccacfd0ec0c1922870a2be503cc7df1ca8be7"},
+ {file = "watchfiles-1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a13ac46b545a7d0d50f7641eefe47d1597e7d1783a5d89e09d080e6dff44b0"},
+ {file = "watchfiles-1.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2bca898c1dc073912d3db7fa6926cc08be9575add9e84872de2c99c688bac4e"},
+ {file = "watchfiles-1.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:06d828fe2adc4ac8a64b875ca908b892a3603d596d43e18f7948f3fef5fc671c"},
+ {file = "watchfiles-1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:074c7618cd6c807dc4eaa0982b4a9d3f8051cd0b72793511848fd64630174b17"},
+ {file = "watchfiles-1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95dc785bc284552d044e561b8f4fe26d01ab5ca40d35852a6572d542adfeb4bc"},
+ {file = "watchfiles-1.0.0.tar.gz", hash = "sha256:37566c844c9ce3b5deb964fe1a23378e575e74b114618d211fbda8f59d7b5dab"},
]
[package.dependencies]
anyio = ">=3.0.0"
[extras]
-inmem = ["langgraph-api"]
+inmem = ["langgraph-api", "python-dotenv"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0,<4.0"
-content-hash = "624dc1a2a5c8a20ef781ed370e106f29da98e7c7235c797ff6a933a3ad20b500"
+content-hash = "3d655bb578e20219e19152d4a3d86be370fe3be61b5559847f0204dfff499b4a"
diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml
index 163bb4a31..fcb3d1979 100644
--- a/libs/cli/pyproject.toml
+++ b/libs/cli/pyproject.toml
@@ -6,7 +6,7 @@ authors = []
license = "MIT"
readme = "README.md"
repository = "https://www.github.com/langchain-ai/langgraph"
-packages = [{include = "langgraph_cli"}]
+packages = [{ include = "langgraph_cli" }]
[tool.poetry.scripts]
langgraph = "langgraph_cli.cli:cli"
@@ -14,7 +14,8 @@ langgraph = "langgraph_cli.cli:cli"
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
click = "^8.1.7"
-langgraph-api = { version = ">=0.0.2,<0.1.0", optional = true , python=">=3.11,<4.0" }
+langgraph-api = { version = ">=0.0.2,<0.1.0", optional = true, python = ">=3.11,<4.0" }
+python-dotenv = { version = ">=0.8.0", optional = true }
[tool.poetry.group.dev.dependencies]
ruff = "^0.6.2"
@@ -26,7 +27,7 @@ pytest-watch = "^4.2.0"
mypy = "^1.10.0"
[tool.poetry.extras]
-inmem = ["langgraph-api"]
+inmem = ["langgraph-api", "python-dotenv"]
[tool.pytest.ini_options]
# --strict-markers will raise errors on unknown marks.
@@ -56,4 +57,4 @@ lint.select = [
# isort
"I",
]
-lint.ignore = [ "E501", "B008" ]
+lint.ignore = ["E501", "B008"]
From 2ee279a977745d0f4e3919fdf0f06471fe0d3293 Mon Sep 17 00:00:00 2001
From: bracesproul
Date: Tue, 26 Nov 2024 11:26:18 -0800
Subject: [PATCH 049/149] fix(sdk-js): Add typing for interrupts on threads
---
libs/sdk-js/src/schema.ts | 20 ++++++++++++++------
1 file changed, 14 insertions(+), 6 deletions(-)
diff --git a/libs/sdk-js/src/schema.ts b/libs/sdk-js/src/schema.ts
index 1b9dae1fe..8884708e4 100644
--- a/libs/sdk-js/src/schema.ts
+++ b/libs/sdk-js/src/schema.ts
@@ -137,6 +137,16 @@ export interface AssistantGraph {
}>;
}
+/**
+ * An interrupt thrown inside a thread.
+ */
+export interface Interrupt {
+ value: unknown;
+ when: "during";
+ resumable: boolean;
+ ns?: string[];
+}
+
export interface Thread {
/** The ID of the thread. */
thread_id: string;
@@ -155,6 +165,9 @@ export interface Thread {
/** The current state of the thread. */
values: ValuesType;
+
+ /** Interrupts which were thrown in this thread */
+ interrupts: {} | { [id: string]: Array };
}
export interface Cron {
@@ -210,12 +223,7 @@ export interface ThreadTask {
name: string;
result?: unknown;
error: Optional;
- interrupts: Array<{
- value: unknown;
- when: "during";
- resumable: boolean;
- ns?: string[];
- }>;
+ interrupts: Array;
checkpoint: Optional;
state: Optional;
}
From 58b99c899e383114d789feb46abb2982bd61b124 Mon Sep 17 00:00:00 2001
From: bracesproul
Date: Tue, 26 Nov 2024 11:28:19 -0800
Subject: [PATCH 050/149] cr
---
libs/sdk-js/src/schema.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/sdk-js/src/schema.ts b/libs/sdk-js/src/schema.ts
index 8884708e4..dd79e07ba 100644
--- a/libs/sdk-js/src/schema.ts
+++ b/libs/sdk-js/src/schema.ts
@@ -167,7 +167,7 @@ export interface Thread {
values: ValuesType;
/** Interrupts which were thrown in this thread */
- interrupts: {} | { [id: string]: Array };
+ interrupts: Record>;
}
export interface Cron {
From 376c58ff3b48abdafa5a68195264f8b68687af0c Mon Sep 17 00:00:00 2001
From: bracesproul
Date: Tue, 26 Nov 2024 11:28:50 -0800
Subject: [PATCH 051/149] expose interupt type
---
libs/sdk-js/src/index.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/libs/sdk-js/src/index.ts b/libs/sdk-js/src/index.ts
index f86406100..764abbc67 100644
--- a/libs/sdk-js/src/index.ts
+++ b/libs/sdk-js/src/index.ts
@@ -15,6 +15,7 @@ export type {
ThreadStatus,
Cron,
Checkpoint,
+ Interrupt,
} from "./schema.js";
export type { OnConflictBehavior, Command } from "./types.js";
From a1ec55abc55c2be71df952095b64ecc3738a3787 Mon Sep 17 00:00:00 2001
From: bracesproul
Date: Tue, 26 Nov 2024 11:40:47 -0800
Subject: [PATCH 052/149] fix(sdk-py): Add typing for interrupts
---
libs/sdk-py/langgraph_sdk/schema.py | 19 +++++++++++++++++--
1 file changed, 17 insertions(+), 2 deletions(-)
diff --git a/libs/sdk-py/langgraph_sdk/schema.py b/libs/sdk-py/langgraph_sdk/schema.py
index 5264ce709..f722f6232 100644
--- a/libs/sdk-py/langgraph_sdk/schema.py
+++ b/libs/sdk-py/langgraph_sdk/schema.py
@@ -1,7 +1,7 @@
"""Data models for interacting with the LangGraph API."""
from datetime import datetime
-from typing import Any, Literal, NamedTuple, Optional, Sequence, TypedDict, Union
+from typing import Any, Dict, Literal, NamedTuple, Optional, Sequence, TypedDict, Union
Json = Optional[dict[str, Any]]
"""Represents a JSON-like structure, which can be None or a dictionary with string keys and any values."""
@@ -176,6 +176,19 @@ class Assistant(AssistantBase):
"""The name of the assistant"""
+class Interrupt(TypedDict, total=False):
+ """Represents an interruption in the execution flow."""
+
+ value: Any
+ """The value associated with the interrupt."""
+ when: Literal["during"]
+ """When the interrupt occurred."""
+ resumable: bool
+ """Whether the interrupt can be resumed."""
+ ns: Optional[list[str]]
+ """Optional namespace for the interrupt."""
+
+
class Thread(TypedDict):
"""Represents a conversation thread."""
@@ -191,6 +204,8 @@ class Thread(TypedDict):
"""The status of the thread, one of 'idle', 'busy', 'interrupted'."""
values: Json
"""The current state of the thread."""
+ interrupts: Dict[str, list[Interrupt]]
+ """Interrupts which were thrown in this thread"""
class ThreadTask(TypedDict):
@@ -199,7 +214,7 @@ class ThreadTask(TypedDict):
id: str
name: str
error: Optional[str]
- interrupts: list[dict]
+ interrupts: list[Interrupt]
checkpoint: Optional[Checkpoint]
state: Optional["ThreadState"]
result: Optional[dict[str, Any]]
From d3a4865c0e7bd9b4db455549d4b193b41803592d Mon Sep 17 00:00:00 2001
From: bracesproul
Date: Tue, 26 Nov 2024 11:42:20 -0800
Subject: [PATCH 053/149] release(sdk-js): 0.0.27
---
libs/sdk-js/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/sdk-js/package.json b/libs/sdk-js/package.json
index bc37a9d4e..3e880cee7 100644
--- a/libs/sdk-js/package.json
+++ b/libs/sdk-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
- "version": "0.0.26",
+ "version": "0.0.27",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
From 16b955dee209f36ff19f7f3367725b538a75cd99 Mon Sep 17 00:00:00 2001
From: jacoblee93
Date: Tue, 26 Nov 2024 12:30:33 -0800
Subject: [PATCH 054/149] Adds fallback for fetching environment variables
---
libs/sdk-js/src/client.ts | 3 ++-
libs/sdk-js/src/utils/env.ts | 11 +++++++++++
2 files changed, 13 insertions(+), 1 deletion(-)
create mode 100644 libs/sdk-js/src/utils/env.ts
diff --git a/libs/sdk-js/src/client.ts b/libs/sdk-js/src/client.ts
index 4829a831f..010864419 100644
--- a/libs/sdk-js/src/client.ts
+++ b/libs/sdk-js/src/client.ts
@@ -33,6 +33,7 @@ import {
OnConflictBehavior,
} from "./types.js";
import { mergeSignals } from "./utils/signals.js";
+import { getEnvironmentVariable } from "./utils/env.js";
/**
* Get the API key from the environment.
@@ -53,7 +54,7 @@ export function getApiKey(apiKey?: string): string | undefined {
const prefixes = ["LANGGRAPH", "LANGSMITH", "LANGCHAIN"];
for (const prefix of prefixes) {
- const envKey = process.env[`${prefix}_API_KEY`];
+ const envKey = getEnvironmentVariable(`${prefix}_API_KEY`);
if (envKey) {
// Remove surrounding quotes
return envKey.trim().replace(/^["']|["']$/g, "");
diff --git a/libs/sdk-js/src/utils/env.ts b/libs/sdk-js/src/utils/env.ts
new file mode 100644
index 000000000..738c14fd5
--- /dev/null
+++ b/libs/sdk-js/src/utils/env.ts
@@ -0,0 +1,11 @@
+export function getEnvironmentVariable(name: string): string | undefined {
+ // Certain setups (Deno, frontend) will throw an error if you try to access environment variables
+ try {
+ return typeof process !== "undefined"
+ ? // eslint-disable-next-line no-process-env
+ process.env?.[name]
+ : undefined;
+ } catch (e) {
+ return undefined;
+ }
+}
From c6a953c02a5e00189c4298f4ec2bf3fdd16e832a Mon Sep 17 00:00:00 2001
From: jacoblee93
Date: Tue, 26 Nov 2024 12:31:00 -0800
Subject: [PATCH 055/149] Bump version
---
libs/sdk-js/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/sdk-js/package.json b/libs/sdk-js/package.json
index 3e880cee7..e1ce9e4ed 100644
--- a/libs/sdk-js/package.json
+++ b/libs/sdk-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
- "version": "0.0.27",
+ "version": "0.0.28",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
From 61e47cb137ed8735e863a54d9ef13deabf30f633 Mon Sep 17 00:00:00 2001
From: vbarda
Date: Wed, 27 Nov 2024 09:37:22 -0500
Subject: [PATCH 056/149] langgraph: relax graph validation to handle nodes
without return typehints
---
libs/langgraph/langgraph/graph/graph.py | 10 +++++---
libs/langgraph/tests/test_pregel.py | 33 +++++++++++++++----------
2 files changed, 26 insertions(+), 17 deletions(-)
diff --git a/libs/langgraph/langgraph/graph/graph.py b/libs/langgraph/langgraph/graph/graph.py
index e91ac4a47..37fe90ebe 100644
--- a/libs/langgraph/langgraph/graph/graph.py
+++ b/libs/langgraph/langgraph/graph/graph.py
@@ -374,6 +374,11 @@ class Graph:
if source not in self.nodes and source != START:
raise ValueError(f"Found edge starting at unknown node '{source}'")
+ if START not in all_sources:
+ raise ValueError(
+ "Graph must have an entrypoint: add at least one edge from START to another node"
+ )
+
# assemble targets
all_targets = {end for _, end in self._all_edges}
for start, branches in self.branches.items():
@@ -392,13 +397,10 @@ class Graph:
for node in self.nodes:
if node != start and node != branch.then:
all_targets.add(node)
+
for name, spec in self.nodes.items():
if spec.ends:
all_targets.update(spec.ends)
- # validate targets
- for node in self.nodes:
- if node not in all_targets:
- raise ValueError(f"Node `{node}` is not reachable")
for target in all_targets:
if target not in self.nodes and target != END:
raise ValueError(f"Found edge ending at unknown node `{target}`")
diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py
index fda881b15..f5a5cd673 100644
--- a/libs/langgraph/tests/test_pregel.py
+++ b/libs/langgraph/tests/test_pregel.py
@@ -164,7 +164,7 @@ def test_graph_validation() -> None:
workflow = Graph()
workflow.add_node("agent", logic)
workflow.set_finish_point("agent")
- with pytest.raises(ValueError, match="not reachable"):
+ with pytest.raises(ValueError, match="must have an entrypoint"):
workflow.compile()
workflow = Graph()
@@ -211,18 +211,6 @@ def test_graph_validation() -> None:
with pytest.raises(ValueError, match="unknown"): # extra is not defined
workflow.compile()
- workflow = Graph()
- workflow.add_node("agent", logic)
- workflow.add_node("tools", logic)
- workflow.add_node("extra", logic)
- workflow.set_entry_point("agent")
- workflow.add_conditional_edges("agent", logic, {"continue": "tools", "exit": END})
- workflow.add_edge("tools", "agent")
- with pytest.raises(
- ValueError, match="Node `extra` is not reachable"
- ): # extra is not reachable
- workflow.compile()
-
workflow = Graph()
workflow.add_node("agent", logic)
workflow.add_node("tools", logic)
@@ -280,6 +268,25 @@ def test_graph_validation() -> None:
graph.invoke({"hello": "there"})
+def test_graph_validation_with_command() -> None:
+ class State(TypedDict):
+ foo: str
+ bar: str
+
+ def node_a(state: State):
+ return GraphCommand(goto="b", update={"foo": "bar"})
+
+ def node_b(state: State):
+ return GraphCommand(goto=END, update={"bar": "baz"})
+
+ builder = StateGraph(State)
+ builder.add_node("a", node_a)
+ builder.add_node("b", node_b)
+ builder.add_edge(START, "a")
+ graph = builder.compile()
+ assert graph.invoke({"foo": ""}) == {"foo": "bar", "bar": "baz"}
+
+
def test_checkpoint_errors() -> None:
class FaultyGetCheckpointer(MemorySaver):
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
From f416480e9d81eaf3018eda2e80facd87a3251252 Mon Sep 17 00:00:00 2001
From: vbarda
Date: Wed, 27 Nov 2024 10:01:24 -0500
Subject: [PATCH 057/149] nit
---
libs/langgraph/langgraph/graph/graph.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/libs/langgraph/langgraph/graph/graph.py b/libs/langgraph/langgraph/graph/graph.py
index 37fe90ebe..50a4dadfb 100644
--- a/libs/langgraph/langgraph/graph/graph.py
+++ b/libs/langgraph/langgraph/graph/graph.py
@@ -397,7 +397,6 @@ class Graph:
for node in self.nodes:
if node != start and node != branch.then:
all_targets.add(node)
-
for name, spec in self.nodes.items():
if spec.ends:
all_targets.update(spec.ends)
From 5144b8f374dd18d7ebd8ab6b75b73685b8e1ea62 Mon Sep 17 00:00:00 2001
From: Vadym Barda
Date: Wed, 27 Nov 2024 12:54:59 -0500
Subject: [PATCH 058/149] langgraph: allow create_react_agent to take empty
tools (#2553)
---
.../langgraph/prebuilt/chat_agent_executor.py | 40 +++++++++++++------
libs/langgraph/tests/test_prebuilt.py | 3 ++
2 files changed, 31 insertions(+), 12 deletions(-)
diff --git a/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py b/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py
index fc812ccbc..4c8699360 100644
--- a/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py
+++ b/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py
@@ -212,6 +212,7 @@ def create_react_agent(
Args:
model: The `LangChain` chat model that supports tool calling.
tools: A list of tools, a ToolExecutor, or a ToolNode instance.
+ If an empty list is provided, the agent will consist of a single LLM node without tool calling.
state_schema: An optional state schema that defines graph state.
Must have `messages` and `is_last_step` keys.
Defaults to `AgentState` that defines those two keys.
@@ -540,19 +541,10 @@ def create_react_agent(
# get the tool functions wrapped in a tool class from the ToolNode
tool_classes = list(tool_node.tools_by_name.values())
- if _should_bind_tools(model, tool_classes):
- model = cast(BaseChatModel, model).bind_tools(tool_classes)
+ tool_calling_enabled = len(tool_classes) > 0
- # Define the function that determines whether to continue or not
- def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
- messages = state["messages"]
- last_message = messages[-1]
- # If there is no function call, then we finish
- if not isinstance(last_message, AIMessage) or not last_message.tool_calls:
- return "__end__"
- # Otherwise if there is, we continue
- else:
- return "tools"
+ if _should_bind_tools(model, tool_classes) and tool_calling_enabled:
+ model = cast(BaseChatModel, model).bind_tools(tool_classes)
# we're passing store here for validation
preprocessor = _get_model_preprocessing_runnable(
@@ -635,6 +627,30 @@ def create_react_agent(
# We return a list, because this will get added to the existing list
return {"messages": [response]}
+ if not tool_calling_enabled:
+ # Define a new graph
+ workflow = StateGraph(state_schema or AgentState)
+ workflow.add_node("agent", RunnableCallable(call_model, acall_model))
+ workflow.set_entry_point("agent")
+ return workflow.compile(
+ checkpointer=checkpointer,
+ store=store,
+ interrupt_before=interrupt_before,
+ interrupt_after=interrupt_after,
+ debug=debug,
+ )
+
+ # Define the function that determines whether to continue or not
+ def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
+ messages = state["messages"]
+ last_message = messages[-1]
+ # If there is no function call, then we finish
+ if not isinstance(last_message, AIMessage) or not last_message.tool_calls:
+ return "__end__"
+ # Otherwise if there is, we continue
+ else:
+ return "tools"
+
# Define a new graph
workflow = StateGraph(state_schema or AgentState)
diff --git a/libs/langgraph/tests/test_prebuilt.py b/libs/langgraph/tests/test_prebuilt.py
index a6655a451..0997668b2 100644
--- a/libs/langgraph/tests/test_prebuilt.py
+++ b/libs/langgraph/tests/test_prebuilt.py
@@ -102,6 +102,9 @@ class FakeToolCallingModel(BaseChatModel):
tools: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]],
**kwargs: Any,
) -> Runnable[LanguageModelInput, BaseMessage]:
+ if len(tools) == 0:
+ raise ValueError("Must provide at least one tool")
+
tool_dicts = []
for tool in tools:
if not isinstance(tool, BaseTool):
From 7ac365ea846046eb905a4a2fe2aff67381c1dd9f Mon Sep 17 00:00:00 2001
From: Jacob Lee
Date: Wed, 27 Nov 2024 11:33:23 -0800
Subject: [PATCH 059/149] fix(sdk-js): Avoid retrying 402s (#2554)
---
libs/sdk-js/package.json | 2 +-
libs/sdk-js/src/utils/async_caller.ts | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/libs/sdk-js/package.json b/libs/sdk-js/package.json
index e1ce9e4ed..ded99a00b 100644
--- a/libs/sdk-js/package.json
+++ b/libs/sdk-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
- "version": "0.0.28",
+ "version": "0.0.29",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
diff --git a/libs/sdk-js/src/utils/async_caller.ts b/libs/sdk-js/src/utils/async_caller.ts
index d58823ba4..d5de9131a 100644
--- a/libs/sdk-js/src/utils/async_caller.ts
+++ b/libs/sdk-js/src/utils/async_caller.ts
@@ -4,6 +4,7 @@ import PQueueMod from "p-queue";
const STATUS_NO_RETRY = [
400, // Bad Request
401, // Unauthorized
+ 402, // Payment required
403, // Forbidden
404, // Not Found
405, // Method Not Allowed
From 1031e54860c5617e9104592cfa057ee371acc152 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 27 Nov 2024 12:44:31 -0800
Subject: [PATCH 060/149] lib: Add exception note identify node/task
---
libs/langgraph/langgraph/pregel/algo.py | 8 ++++++++
libs/langgraph/langgraph/pregel/retry.py | 6 ++++++
2 files changed, 14 insertions(+)
diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py
index 1410e432f..3c3008ce4 100644
--- a/libs/langgraph/langgraph/pregel/algo.py
+++ b/libs/langgraph/langgraph/pregel/algo.py
@@ -1,3 +1,4 @@
+import sys
from collections import defaultdict, deque
from functools import partial
from hashlib import sha1
@@ -66,6 +67,7 @@ from langgraph.types import All, LoopProtocol, PregelExecutableTask, PregelTask
from langgraph.utils.config import merge_configs, patch_config
GetNextVersion = Callable[[Optional[V], BaseChannel], V]
+SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11)
class WritesProtocol(Protocol):
@@ -634,6 +636,12 @@ def prepare_single_task(
)
except StopIteration:
return
+ except Exception as exc:
+ if SUPPORTS_EXC_NOTES:
+ exc.add_note(
+ f"Before task with name '{name}' and path '{task_path[:3]}'"
+ )
+ raise
# create task id
checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
diff --git a/libs/langgraph/langgraph/pregel/retry.py b/libs/langgraph/langgraph/pregel/retry.py
index 6e52a7c41..2d0f2b6da 100644
--- a/libs/langgraph/langgraph/pregel/retry.py
+++ b/libs/langgraph/langgraph/pregel/retry.py
@@ -1,6 +1,7 @@
import asyncio
import logging
import random
+import sys
import time
from dataclasses import replace
from functools import partial
@@ -18,6 +19,7 @@ from langgraph.types import Command, PregelExecutableTask, RetryPolicy
from langgraph.utils.config import patch_configurable
logger = logging.getLogger(__name__)
+SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11)
def run_with_retry(
@@ -60,6 +62,8 @@ def run_with_retry(
# if interrupted, end
raise
except Exception as exc:
+ if SUPPORTS_EXC_NOTES:
+ exc.add_note(f"During task with name '{task.name}' and id '{task.id}'")
if retry_policy is None:
raise
# increment attempts
@@ -152,6 +156,8 @@ async def arun_with_retry(
# if interrupted, end
raise
except Exception as exc:
+ if SUPPORTS_EXC_NOTES:
+ exc.add_note(f"During task with name '{task.name}' and id '{task.id}'")
if retry_policy is None:
raise
# increment attempts
From dc09b134007c2a8c054b4db6ff09cf779366ebd0 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 27 Nov 2024 14:10:50 -0800
Subject: [PATCH 061/149] sdk-py: Fix SSE parsing to split lines only \n \r
\r\n per SSE spec
---
libs/sdk-py/langgraph_sdk/client.py | 5 +-
libs/sdk-py/langgraph_sdk/sse.py | 106 ++++++++++++++++++++++++++++
2 files changed, 109 insertions(+), 2 deletions(-)
create mode 100644 libs/sdk-py/langgraph_sdk/sse.py
diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py
index 6a3bb6c9c..a018bb3b6 100644
--- a/libs/sdk-py/langgraph_sdk/client.py
+++ b/libs/sdk-py/langgraph_sdk/client.py
@@ -59,6 +59,7 @@ from langgraph_sdk.schema import (
ThreadStatus,
ThreadUpdateStateResponse,
)
+from langgraph_sdk.sse import EventSource
logger = logging.getLogger(__name__)
@@ -292,7 +293,7 @@ class HttpClient:
else:
logger.error(f"Error from langgraph-api: {body}", exc_info=e)
raise e
- async for event in sse.aiter_sse():
+ async for event in EventSource(sse.response).aiter_sse():
yield StreamPart(
event.event, orjson.loads(event.data) if event.data else None
)
@@ -2426,7 +2427,7 @@ class SyncHttpClient:
else:
logger.error(f"Error from langgraph-api: {body}", exc_info=e)
raise e
- for event in sse.iter_sse():
+ for event in EventSource(sse.response).iter_sse():
yield StreamPart(
event.event, orjson.loads(event.data) if event.data else None
)
diff --git a/libs/sdk-py/langgraph_sdk/sse.py b/libs/sdk-py/langgraph_sdk/sse.py
new file mode 100644
index 000000000..b87788387
--- /dev/null
+++ b/libs/sdk-py/langgraph_sdk/sse.py
@@ -0,0 +1,106 @@
+"""Adapted from httpx_sse to split lines on \n, \r, \r\n per the SSE spec."""
+
+import io
+from typing import AsyncIterator, Iterator
+
+import httpx
+import httpx_sse
+import httpx_sse._decoders
+
+
+class BytesLineDecoder:
+ """
+ Handles incrementally reading lines from text.
+
+ Has the same behaviour as the stdllib bytes splitlines,
+ but handling the input iteratively.
+ """
+
+ def __init__(self) -> None:
+ self.buffer = io.BytesIO()
+ self.trailing_cr: bool = False
+
+ def decode(self, text: bytes) -> list[bytes]:
+ # See https://docs.python.org/3/glossary.html#term-universal-newlines
+ NEWLINE_CHARS = b"\n\r"
+
+ # We always push a trailing `\r` into the next decode iteration.
+ if self.trailing_cr:
+ text = b"\r" + text
+ self.trailing_cr = False
+ if text.endswith(b"\r"):
+ self.trailing_cr = True
+ text = text[:-1]
+
+ if not text:
+ # NOTE: the edge case input of empty text doesn't occur in practice,
+ # because other httpx internals filter out this value
+ return [] # pragma: no cover
+
+ trailing_newline = text[-1] in NEWLINE_CHARS
+ lines = text.splitlines()
+
+ if len(lines) == 1 and not trailing_newline:
+ # No new lines, buffer the input and continue.
+ self.buffer.append(lines[0])
+ return []
+
+ if self.buffer:
+ # Include any existing buffer in the first portion of the
+ # splitlines result.
+ lines = [self.buffer.getvalue() + lines[0]] + lines[1:]
+ self.buffer.truncate(0)
+
+ if not trailing_newline:
+ # If the last segment of splitlines is not newline terminated,
+ # then drop it from our output and start a new buffer.
+ self.buffer.write(lines.pop())
+
+ return lines
+
+ def flush(self) -> list[bytes]:
+ if not self.buffer and not self.trailing_cr:
+ return []
+
+ lines = [self.buffer.getvalue()] if self.buffer else []
+ self.buffer.truncate(0)
+ self.trailing_cr = False
+ return lines
+
+
+async def aiter_lines_raw(response: httpx.Response) -> AsyncIterator[bytes]:
+ decoder = BytesLineDecoder()
+ async for chunk in response.aiter_bytes():
+ for line in decoder.decode(chunk):
+ yield line
+ for line in decoder.flush():
+ yield line
+
+
+def iter_lines_raw(response: httpx.Response) -> Iterator[bytes]:
+ decoder = BytesLineDecoder()
+ for chunk in response.iter_bytes():
+ for line in decoder.decode(chunk):
+ yield line
+ for line in decoder.flush():
+ yield line
+
+
+class EventSource(httpx_sse.EventSource):
+ async def aiter_sse(self) -> AsyncIterator[httpx_sse.ServerSentEvent]:
+ self._check_content_type()
+ decoder = httpx_sse._decoders.SSEDecoder()
+ async for line in aiter_lines_raw(self._response):
+ line = line.rstrip(b"\n")
+ sse = decoder.decode(line.decode())
+ if sse is not None:
+ yield sse
+
+ def iter_sse(self) -> Iterator[httpx_sse.ServerSentEvent]:
+ self._check_content_type()
+ decoder = httpx_sse._decoders.SSEDecoder()
+ for line in iter_lines_raw(self._response):
+ line = line.rstrip(b"\n")
+ sse = decoder.decode(line.decode())
+ if sse is not None:
+ yield sse
From 1d9a0d1e4e8d1443aebc54fccd1061ec144114e6 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 27 Nov 2024 14:17:03 -0800
Subject: [PATCH 062/149] sdk-py 0.1.37
---
libs/sdk-py/pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml
index 393750ba6..c9e0401da 100644
--- a/libs/sdk-py/pyproject.toml
+++ b/libs/sdk-py/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
-version = "0.1.36"
+version = "0.1.37"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"
From dfaff2511b73d03329325f6628733b6c02ea624f Mon Sep 17 00:00:00 2001
From: Andrew Nguonly
Date: Wed, 27 Nov 2024 14:39:12 -0800
Subject: [PATCH 063/149] docs: Update API docs and remove unused pages (#2561)
---
.../cloud/reference/api/open_agent_api.json | 2071 -----------------
.../reference/api/open_agent_api_ref.html | 19 -
docs/docs/cloud/reference/api/openapi.json | 21 +-
3 files changed, 15 insertions(+), 2096 deletions(-)
delete mode 100644 docs/docs/cloud/reference/api/open_agent_api.json
delete mode 100644 docs/docs/cloud/reference/api/open_agent_api_ref.html
diff --git a/docs/docs/cloud/reference/api/open_agent_api.json b/docs/docs/cloud/reference/api/open_agent_api.json
deleted file mode 100644
index f78bd420e..000000000
--- a/docs/docs/cloud/reference/api/open_agent_api.json
+++ /dev/null
@@ -1,2071 +0,0 @@
-{
- "openapi": "3.1.0",
- "info": {
- "title": "Open Assistants API Specification",
- "version": "1.0.0"
- },
- "tags": [
- {
- "name": "Templates",
- "description": "A template is the cognitive architecture of an assistant."
- },
- {
- "name": "Assistants",
- "description": "An assistant is a configured instance of a template."
- },
- {
- "name": "Threads",
- "description": "A thread contains the accumulated outputs of a group of runs. The outputs are persisted to a thread's state."
- },
- {
- "name": "Runs",
- "description": "A run is an invocation of an assistant. The output of a run is persisted to a thread's state."
- },
- {
- "name": "Runs (Threadless)",
- "description": "A run is an invocation of an assistant. The output of a threadless run is not persisted to any thread state."
- }
- ],
- "paths": {
- "/templates": {
- "get": {
- "tags": [
- "Templates"
- ],
- "summary": "List Templates",
- "description": "List all templates.",
- "operationId": "templates_get",
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "$ref": "#/components/schemas/Template"
- },
- "type": "array"
- }
- }
- }
- }
- }
- }
- },
- "/assistants": {
- "post": {
- "tags": [
- "Assistants"
- ],
- "summary": "Create Assistant",
- "description": "Create an assistant.\n\nAn initial version of the assistant will be created and the assistant is set to that version. To change versions, use the `PATCH /assistants/{assistant_id}/` endpoint.",
- "operationId": "assistants_post",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/AssistantCreate"
- }
- }
- },
- "required": true
- },
- "responses": {
- "201": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Assistant"
- }
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/assistants/search": {
- "post": {
- "tags": [
- "Assistants"
- ],
- "summary": "Search Assistants",
- "description": "Search for assistants.\n\nThis endpoint also functions as the endpoint to list all assistants (omit `metadata` and `template_id`). The API specification does not specify how the search is implemented.",
- "operationId": "assistants_search_post",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/AssistantSearch"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "$ref": "#/components/schemas/Assistant"
- },
- "type": "array",
- "title": "Response Search Assistants Assistants Search Post"
- }
- }
- }
- }
- }
- }
- },
- "/assistants/{assistant_id}": {
- "get": {
- "tags": [
- "Assistants"
- ],
- "summary": "Get Assistant",
- "description": "Get an assistant by ID.",
- "operationId": "assistants__assistant_id__get",
- "parameters": [
- {
- "description": "The ID of the assistant.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Assistant ID",
- "description": "The ID of the assistant."
- },
- "name": "assistant_id",
- "in": "path"
- }
- ],
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Assistant"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- },
- "patch": {
- "tags": [
- "Assistants"
- ],
- "summary": "Patch Assistant",
- "description": "Patch an assistant by ID.",
- "operationId": "assistants__assistant_id__patch",
- "parameters": [
- {
- "description": "The ID of the assistant.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Assistant ID",
- "description": "The ID of the assistant."
- },
- "name": "assistant_id",
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/AssistantPatch"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Assistant"
- }
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- },
- "delete": {
- "tags": [
- "Assistants"
- ],
- "summary": "Delete Assistant",
- "description": "Delete an assistant by ID.\n\nAll versions of the assistant will be deleted as well.",
- "operationId": "assistants__assistant_id__delete",
- "parameters": [
- {
- "description": "The ID of the assistant.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Assistant ID",
- "description": "The ID of the assistant."
- },
- "name": "assistant_id",
- "in": "path"
- }
- ],
- "responses": {
- "204": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": null
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/assistants/{assistant_id}/versions": {
- "post": {
- "tags": [
- "Assistants"
- ],
- "summary": "Create Assistant Version",
- "description": "Create a new version of an assistant.\n\nAn assistant version is immutable. Assistant versions can only be created.",
- "operationId": "assistants__assistant_id__versions_post",
- "parameters": [
- {
- "description": "The ID of the assistant.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Assistant Id",
- "description": "The ID of the assistant."
- },
- "name": "assistant_id",
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/AssistantVersionCreate"
- }
- }
- },
- "required": true
- },
- "responses": {
- "201": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Assistant"
- }
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/assistants/{assistant_id}/versions/search": {
- "post": {
- "tags": [
- "Assistants"
- ],
- "summary": "Search Assistant Versions",
- "description": "Search for assistant versions.\n\nThis endpoint also functions as the endpoint to list all versions of an assistant (omit `metadata` and `template_id`). The API specification does not specify how the search is implemented.",
- "operationId": "assistants__assistant_id__versions_search_post",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/AssistantSearch"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "$ref": "#/components/schemas/Assistant"
- },
- "type": "array",
- "title": "Response Search Assistants Assistants Search Post"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/assistants/{assistant_id}/versions/{version}": {
- "get": {
- "tags": [
- "Assistants"
- ],
- "summary": "Get Assistant Version",
- "description": "Get a version of an assistant.",
- "operationId": "assistants__assistant_id__versions__version__get",
- "parameters": [
- {
- "description": "The ID of the assistant.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Assistant Id",
- "description": "The ID of the assistant."
- },
- "name": "assistant_id",
- "in": "path"
- },
- {
- "description": "The version of the assistant.",
- "required": true,
- "schema": {
- "type": "integer",
- "title": "Version",
- "description": "The version of the assistant."
- },
- "name": "version",
- "in": "path"
- }
- ],
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Assistant"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/threads": {
- "post": {
- "tags": [
- "Threads"
- ],
- "summary": "Create Thread",
- "description": "Create a thread.",
- "operationId": "threads_post",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ThreadCreate"
- }
- }
- },
- "required": true
- },
- "responses": {
- "201": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Thread"
- }
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/threads/search": {
- "post": {
- "tags": [
- "Threads"
- ],
- "summary": "Search Threads",
- "description": "Search for threads.\n\nThis endpoint also functions as the endpoint to list all threads (omit `metadata`, `values`, and `status`). The API specification does not specify how the search is implemented.",
- "operationId": "threads_search_post",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ThreadSearch"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "$ref": "#/components/schemas/Thread"
- },
- "type": "array",
- "title": "Response Search Threads Threads Search Post"
- }
- }
- }
- }
- }
- }
- },
- "/threads/{thread_id}": {
- "get": {
- "tags": [
- "Threads"
- ],
- "summary": "Get Thread",
- "description": "Get a thread by ID.",
- "operationId": "threads__thread_id__get",
- "parameters": [
- {
- "description": "The ID of the thread.",
- "required": true,
- "schema": {
- "type": "string",
- "format": "uuid",
- "title": "Thread Id",
- "description": "The ID of the thread."
- },
- "name": "thread_id",
- "in": "path"
- }
- ],
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Thread"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- },
- "patch": {
- "tags": [
- "Threads"
- ],
- "summary": "Patch Thread",
- "description": "Patch a thread by ID.",
- "operationId": "threads__thread_id__patch",
- "parameters": [
- {
- "description": "The ID of the thread.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Thread Id",
- "description": "The ID of the thread."
- },
- "name": "thread_id",
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ThreadPatch"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Thread"
- }
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- },
- "delete": {
- "tags": [
- "Threads"
- ],
- "summary": "Delete Thread",
- "description": "Delete a thread by ID.",
- "operationId": "threads__thread_id__delete",
- "parameters": [
- {
- "description": "The ID of the thread.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Thread Id",
- "description": "The ID of the thread."
- },
- "name": "thread_id",
- "in": "path"
- }
- ],
- "responses": {
- "204": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": null
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/threads/{thread_id}/state": {
- "post": {
- "tags": [
- "Threads"
- ],
- "summary": "Create Thread State",
- "description": "Add state to a thread.",
- "operationId": "threads__thread_id__state_post",
- "parameters": [
- {
- "description": "The ID of the thread.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Thread ID",
- "description": "The ID of the thread."
- },
- "name": "thread_id",
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ThreadStateCreate"
- }
- }
- },
- "required": true
- },
- "responses": {
- "201": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ThreadState"
- }
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- },
- "get": {
- "tags": [
- "Threads"
- ],
- "summary": "Get Thread State",
- "description": "Get state for a thread.\n\nThe latest state of the thread is returned.",
- "operationId": "threads__thread_id__state_get",
- "parameters": [
- {
- "description": "The ID of the thread.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Thread ID",
- "description": "The ID of the thread."
- },
- "name": "thread_id",
- "in": "path"
- }
- ],
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ThreadState"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/threads/{thread_id}/state/search": {
- "post": {
- "tags": [
- "Threads"
- ],
- "summary": "Search Thread States",
- "description": "Search for thread states.\n\nThis endpoint also functions as the endpoint to list all thread states (omit `metadata` and `checkpoint_id`). The API specification does not specify how the search is implemented.",
- "operationId": "threads__thread_id__state_search_post",
- "parameters": [
- {
- "description": "The ID of the thread.",
- "required": true,
- "schema": {
- "type": "string",
- "format": "uuid",
- "title": "Thread Id",
- "description": "The ID of the thread."
- },
- "name": "thread_id",
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ThreadStateSearch"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "$ref": "#/components/schemas/ThreadState"
- },
- "type": "array",
- "title": "Response Get Thread History Post Threads Thread Id History Post"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/threads/{thread_id}/runs": {
- "post": {
- "tags": [
- "Runs"
- ],
- "summary": "Create Run",
- "description": "Create a run and persist its output to a thread. Don't wait for the final output. Return immediately.",
- "operationId": "threads__thread_id__runs_post",
- "parameters": [
- {
- "description": "The ID of the thread.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Thread ID",
- "description": "The ID of the thread."
- },
- "name": "thread_id",
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RunCreate"
- }
- }
- },
- "required": true
- },
- "responses": {
- "201": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Run"
- }
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- },
- "get": {
- "tags": [
- "Runs"
- ],
- "summary": "List Runs",
- "description": "Get runs for a thread.",
- "operationId": "threads__thread_id__runs_get",
- "parameters": [
- {
- "description": "The ID of the thread.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Thread ID",
- "description": "The ID of the thread."
- },
- "name": "thread_id",
- "in": "path"
- },
- {
- "required": false,
- "schema": {
- "type": "integer",
- "title": "Limit",
- "default": 10
- },
- "name": "limit",
- "in": "query"
- },
- {
- "required": false,
- "schema": {
- "type": "integer",
- "title": "Offset",
- "default": 0
- },
- "name": "offset",
- "in": "query"
- }
- ],
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "$ref": "#/components/schemas/Run"
- },
- "type": "array"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/threads/{thread_id}/runs/stream": {
- "post": {
- "tags": [
- "Runs"
- ],
- "summary": "Create Run, Stream Output",
- "description": "Create a run and persist its output to a thread. Stream the output.",
- "operationId": "threads__thread_id__runs_stream_post",
- "parameters": [
- {
- "description": "The ID of the thread.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Thread Id",
- "description": "The ID of the thread."
- },
- "name": "thread_id",
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RunCreate"
- }
- }
- },
- "required": true
- },
- "responses": {
- "201": {
- "description": "Success",
- "content": {
- "text/event-stream": {
- "schema": {
- "type": "string",
- "description": "The server will send a stream of events in SSE format.\n\n**Example event**:\n\nid: 1\n\nevent: message\n\ndata: {}"
- }
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/threads/{thread_id}/runs/wait": {
- "post": {
- "tags": [
- "Runs"
- ],
- "summary": "Create Run, Wait for Output",
- "description": "Create a run and persist its output to a thread. Wait for the final output and then return.",
- "operationId": "threads__thread_id__runs_wait_post",
- "parameters": [
- {
- "description": "The ID of the thread.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Thread ID",
- "description": "The ID of the thread."
- },
- "name": "thread_id",
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RunCreate"
- }
- }
- },
- "required": true
- },
- "responses": {
- "201": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RunWaitOutput"
- }
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/threads/{thread_id}/runs/{run_id}": {
- "get": {
- "tags": [
- "Runs"
- ],
- "summary": "Get Run",
- "description": "Get a run by ID.",
- "operationId": "threads__thread_id__runs__run_id__get",
- "parameters": [
- {
- "description": "The ID of the thread.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Thread ID",
- "description": "The ID of the thread."
- },
- "name": "thread_id",
- "in": "path"
- },
- {
- "description": "The ID of the run.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Run ID",
- "description": "The ID of the run."
- },
- "name": "run_id",
- "in": "path"
- }
- ],
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Run"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- },
- "delete": {
- "tags": [
- "Runs"
- ],
- "summary": "Delete Run",
- "description": "Delete a run by ID.",
- "operationId": "threads__thread_id__runs__run_id__delete",
- "parameters": [
- {
- "description": "The ID of the thread.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Thread ID",
- "description": "The ID of the thread."
- },
- "name": "thread_id",
- "in": "path"
- },
- {
- "description": "The ID of the run.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Run ID",
- "description": "The ID of the run."
- },
- "name": "run_id",
- "in": "path"
- }
- ],
- "responses": {
- "204": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": null
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/threads/{thread_id}/runs/{run_id}/cancel": {
- "post": {
- "tags": [
- "Runs"
- ],
- "summary": "Cancel Run",
- "description": "Cancel a run by ID.",
- "operationId": "threads__thread_id__runs__run_id__cancel_post",
- "parameters": [
- {
- "description": "The ID of the thread.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Thread ID",
- "description": "The ID of the thread."
- },
- "name": "thread_id",
- "in": "path"
- },
- {
- "description": "The ID of the run.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Run ID",
- "description": "The ID of the run."
- },
- "name": "run_id",
- "in": "path"
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RunCancel"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": null
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/threads/{thread_id}/runs/{run_id}/stream": {
- "get": {
- "tags": [
- "Runs"
- ],
- "summary": "Stream Output of Run",
- "description": "Stream the output of a run.\n\nOnly output produced after this endpoint is called will be streamed.",
- "operationId": "threads__thread_id__runs__run_id__join_get",
- "parameters": [
- {
- "description": "The ID of the thread.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Thread ID",
- "description": "The ID of the thread."
- },
- "name": "thread_id",
- "in": "path"
- },
- {
- "description": "The ID of the run.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Run ID",
- "description": "The ID of the run."
- },
- "name": "run_id",
- "in": "path"
- }
- ],
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "text/event-stream": {
- "schema": {
- "type": "string",
- "description": "The server will send a stream of events in SSE format.\n\n**Example event**:\n\nid: 1\n\nevent: message\n\ndata: {}"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/threads/{thread_id}/runs/{run_id}/wait": {
- "get": {
- "tags": [
- "Runs"
- ],
- "summary": "Wait for Output of Run",
- "description": "Wait for the final output of a run and then return.",
- "operationId": "threads__thread_id__runs__run_id__join_get",
- "parameters": [
- {
- "description": "The ID of the thread.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Thread ID",
- "description": "The ID of the thread."
- },
- "name": "thread_id",
- "in": "path"
- },
- {
- "description": "The ID of the run.",
- "required": true,
- "schema": {
- "type": "string",
- "title": "Run ID",
- "description": "The ID of the run."
- },
- "name": "run_id",
- "in": "path"
- }
- ],
- "responses": {
- "200": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RunWaitOutput"
- }
- }
- }
- },
- "404": {
- "description": "Not Found",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/runs": {
- "post": {
- "tags": [
- "Runs (Threadless)"
- ],
- "summary": "Create Run",
- "description": "Create a run without persisting its output to a thread. Don't wait for the final output. Return immediately.",
- "operationId": "runs_post",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RunCreate"
- }
- }
- },
- "required": true
- },
- "responses": {
- "201": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": null
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/runs/stream": {
- "post": {
- "tags": [
- "Runs (Threadless)"
- ],
- "summary": "Create Run, Stream Output",
- "description": "Create a run without persisting its output to a thread. Stream the output.",
- "operationId": "runs_stream_post",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RunCreate"
- }
- }
- },
- "required": true
- },
- "responses": {
- "201": {
- "description": "Success",
- "content": {
- "text/event-stream": {
- "schema": {
- "type": "string",
- "description": "The server will send a stream of events in SSE format.\n\n**Example event**:\n\nid: 1\n\nevent: message\n\ndata: {}"
- }
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/runs/wait": {
- "post": {
- "tags": [
- "Runs (Threadless)"
- ],
- "summary": "Create Run, Wait for Output",
- "description": "Create a run without persisting its output to a thread. Wait for the final output and then return.",
- "operationId": "runs_wait_post",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RunCreate"
- }
- }
- },
- "required": true
- },
- "responses": {
- "201": {
- "description": "Success",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RunWaitOutput"
- }
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- }
- },
- "components": {
- "schemas": {
- "Template": {
- "properties": {
- "template_id": {
- "type": "string",
- "title": "Template ID"
- }
- },
- "type": "object",
- "required": [
- "template_id"
- ],
- "title": "Template"
- },
- "Assistant": {
- "properties": {
- "assistant_id": {
- "type": "string",
- "title": "Assistant ID"
- },
- "template_id": {
- "type": "string",
- "title": "Template ID"
- },
- "config": {
- "type": "object",
- "title": "Config"
- },
- "metadata": {
- "type": "object",
- "title": "Metadata"
- },
- "version": {
- "type": "integer",
- "title": "Version"
- },
- "created_at": {
- "type": "string",
- "format": "date-time",
- "title": "Created At"
- },
- "updated_at": {
- "type": "string",
- "format": "date-time",
- "title": "Updated At"
- }
- },
- "type": "object",
- "required": [
- "assistant_id",
- "graph_id",
- "config",
- "created_at",
- "updated_at",
- "metadata"
- ],
- "title": "Assistant"
- },
- "AssistantCreate": {
- "properties": {
- "assistant_id": {
- "type": "string",
- "title": "Assistant ID",
- "description": "The ID of the assistant. If not provided, an ID is generated."
- },
- "template_id": {
- "type": "string",
- "title": "Template ID",
- "description": "The Template ID references an internal template implementation for the assistant."
- },
- "config": {
- "type": "object",
- "title": "Config",
- "description": "Arbitrary configuration for the assistant. The configuration may augment the behavior of the assistant depending on the referenced template."
- },
- "metadata": {
- "type": "object",
- "title": "Metadata",
- "description": "Arbitrary metadata for the assistant."
- }
- },
- "type": "object",
- "required": [
- "template_id"
- ],
- "title": "AssistantCreate",
- "description": "Payload for creating an assistant."
- },
- "AssistantPatch": {
- "properties": {
- "version": {
- "type": "integer",
- "title": "Version",
- "description": "Version to change to."
- }
- },
- "type": "integer",
- "required": [
- "version"
- ],
- "title": "AssistantPatch",
- "description": "Payload for patching an assistant."
- },
- "AssistantSearch": {
- "properties": {
- "metadata": {
- "type": "object",
- "title": "Metadata",
- "description": "Metadata to search for."
- },
- "template_id": {
- "type": "string",
- "title": "Template ID",
- "description": "Filter by template ID."
- },
- "limit": {
- "type": "integer",
- "title": "Limit",
- "description": "Maximum number to return.",
- "default": 10,
- "minimum": 1,
- "maximum": 1000
- },
- "offset": {
- "type": "integer",
- "title": "Offset",
- "description": "Offset to start from.",
- "default": 0,
- "minimum": 0
- }
- },
- "type": "object",
- "title": "AssistantSearch",
- "description": "Payload for searching for assistants or assistant versions."
- },
- "AssistantVersionCreate": {
- "properties": {
- "template_id": {
- "type": "string",
- "title": "Template ID",
- "description": "The Template ID references an internal template implementation for the assistant."
- },
- "config": {
- "type": "object",
- "title": "Config",
- "description": "Arbitrary configuration for the assistant. The configuration may augment the behavior of the assistant depending on the referenced template."
- },
- "metadata": {
- "type": "object",
- "title": "Metadata",
- "description": "Arbitrary metadata for the assistant."
- }
- },
- "type": "object",
- "title": "AssistantVersionCreate",
- "description": "Payload for creating an assistant version."
- },
- "Thread": {
- "properties": {
- "thread_id": {
- "type": "string",
- "title": "Thread ID"
- },
- "metadata": {
- "type": "object",
- "title": "Metadata",
- "description": "Arbitrary metadata for the thread."
- },
- "status": {
- "type": "string",
- "enum": [
- "idle",
- "busy",
- "interrupted",
- "error"
- ],
- "title": "Status",
- "description": "The status indicates the current state of the thread with respect to \"double texting\" use cases."
- },
- "values": {
- "type": "object",
- "title": "Values",
- "description": "Arbitrary state values persisted to the thread."
- },
- "created_at": {
- "type": "string",
- "format": "date-time",
- "title": "Created At"
- },
- "updated_at": {
- "type": "string",
- "format": "date-time",
- "title": "Updated At"
- }
- },
- "type": "object",
- "required": [
- "thread_id",
- "created_at",
- "updated_at",
- "metadata",
- "status"
- ],
- "title": "Thread"
- },
- "ThreadCreate": {
- "properties": {
- "thread_id": {
- "type": "string",
- "title": "Thread Id",
- "description": "The ID of the thread. If not provided, an ID is generated."
- },
- "metadata": {
- "type": "object",
- "title": "Metadata",
- "description": "Arbitrary metadata for the thread."
- }
- },
- "type": "object",
- "title": "ThreadCreate",
- "description": "Payload for creating a thread."
- },
- "ThreadPatch": {
- "properties": {
- "metadata": {
- "type": "object",
- "title": "Metadata",
- "description": "Arbitrary metadata for the thread."
- }
- },
- "type": "object",
- "title": "ThreadPatch",
- "description": "Payload for patching a thread."
- },
- "ThreadSearch": {
- "properties": {
- "metadata": {
- "type": "object",
- "title": "Metadata",
- "description": "Metadata to search for."
- },
- "values": {
- "type": "object",
- "title": "Values",
- "description": "State values to search for."
- },
- "status": {
- "type": "string",
- "enum": [
- "idle",
- "busy",
- "interrupted",
- "error"
- ],
- "title": "Status",
- "description": "Status to search for.\n\nThe status indicates the current state of the thread with respect to \"double texting\" use cases."
- },
- "limit": {
- "type": "integer",
- "title": "Limit",
- "description": "Maximum number to return.",
- "default": 10,
- "minimum": 1,
- "maximum": 1000
- },
- "offset": {
- "type": "integer",
- "title": "Offset",
- "description": "Offset to start from.",
- "default": 0,
- "minimum": 0
- }
- },
- "type": "object",
- "title": "ThreadSearch",
- "description": "Payload for searching for threads."
- },
- "ThreadState": {
- "properties": {
- "values": {
- "type": "object",
- "title": "Values"
- },
- "checkpoint": {
- "type": "object",
- "properties": {
- "checkpoint_id": {
- "type": "string",
- "title": "Checkpoint ID",
- "description": "The ID of the checkpoint."
- }
- },
- "title": "Checkpoint"
- },
- "parent_checkpoint": {
- "type": "object",
- "properties": {
- "checkpoint_id": {
- "type": "string",
- "title": "Checkpoint ID",
- "description": "The ID of the checkpoint."
- }
- },
- "title": "Parent Checkpoint"
- },
- "metadata": {
- "type": "object",
- "title": "Metadata"
- },
- "created_at": {
- "type": "string",
- "title": "Created At"
- }
- },
- "type": "object",
- "required": [
- "values",
- "next",
- "checkpoint",
- "metadata",
- "created_at"
- ],
- "title": "ThreadState"
- },
- "ThreadStateCreate": {
- "properties": {
- "values": {
- "type": "object",
- "title": "Values"
- },
- "checkpoint": {
- "properties": {
- "checkpoint_id": {
- "type": "string",
- "title": "Checkpoint ID",
- "description": "The ID of the checkpoint."
- }
- },
- "type": "object",
- "title": "Checkpoint"
- }
- },
- "type": "object",
- "title": "ThreadStateCreate",
- "description": "Payload for adding state to a thread."
- },
- "ThreadStateSearch": {
- "properties": {
- "limit": {
- "type": "integer",
- "title": "Limit",
- "description": "The maximum number of states to return.",
- "default": 10,
- "maximum": 1000,
- "minimum": 1
- },
- "offset": {
- "type": "string",
- "title": "Before",
- "description": "Return states before this checkpoint ID."
- },
- "checkpoint_id": {
- "type": "string",
- "title": "Checkpoint ID",
- "description": "Filter by checkpoint ID."
- },
- "metadata": {
- "type": "object",
- "title": "Metadata",
- "description": "Metadata to search for."
- }
- },
- "type": "object",
- "title": "ThreadStateSearch"
- },
- "Run": {
- "properties": {
- "run_id": {
- "type": "string",
- "title": "Run ID"
- },
- "thread_id": {
- "type": "string",
- "title": "Thread ID"
- },
- "assistant_id": {
- "type": "string",
- "title": "Assistant ID"
- },
- "status": {
- "type": "string",
- "enum": [
- "pending",
- "error",
- "success",
- "timeout",
- "interrupted"
- ],
- "title": "Status"
- },
- "metadata": {
- "type": "object",
- "title": "Metadata"
- },
- "multitask_strategy": {
- "type": "string",
- "enum": [
- "reject",
- "rollback",
- "interrupt",
- "enqueue"
- ],
- "title": "Multitask Strategy",
- "description": "The multitask strategy determines the behavior of the run with respect to \"double texting\" use cases.",
- "default": "reject"
- },
- "created_at": {
- "type": "string",
- "format": "date-time",
- "title": "Created At"
- },
- "updated_at": {
- "type": "string",
- "format": "date-time",
- "title": "Updated At"
- }
- },
- "type": "object",
- "required": [
- "run_id",
- "thread_id",
- "assistant_id",
- "created_at",
- "updated_at",
- "status",
- "metadata",
- "kwargs",
- "multitask_strategy"
- ],
- "title": "Run"
- },
- "RunCreate": {
- "properties": {
- "assistant_id": {
- "type": "string",
- "title": "Assistant Id"
- },
- "input": {
- "type": "object",
- "title": "Input",
- "description": "Arbitrary input for the run."
- },
- "metadata": {
- "type": "object",
- "title": "Metadata",
- "description": "Arbitrary metadata for the run."
- },
- "config": {
- "type": "object",
- "title": "Config",
- "description": "Arbitrary configuration for the run. The configuration may augment the behavior of the assistant depending on the referenced template."
- },
- "interrupt_before": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "default": [],
- "title": "Interrupt Before",
- "description": "An arbitrary list of strings that determine how the assistant handles Human-in-the-Loop use cases for **BEFORE** execution workflows. The API specification does not specify the possible values of this field or the default value if Human-in-the-Loop use cases are not supported by the implementation."
- },
- "interrupt_after": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "default": [],
- "title": "Interrupt After",
- "description": "An arbitrary list of strings that determine how the assistant handles Human-in-the-Loop use cases for **AFTER** execution workflows. The API specification does not specify the possible values of this field or the default value if Human-in-the-Loop use cases are not supported by the implementation."
- },
- "stream_mode": {
- "type": "array",
- "items": {
- "type": "string",
- "enum": [
- "values",
- "messages",
- "updates",
- "debug",
- "custom"
- ]
- },
- "title": "Stream Mode",
- "default": [
- "values"
- ]
- },
- "multitask_strategy": {
- "type": "string",
- "enum": [
- "reject",
- "rollback",
- "interrupt",
- "enqueue"
- ],
- "title": "Multitask Strategy",
- "description": "The multitask strategy determines the behavior of the run with respect to \"double texting\" use cases.",
- "default": "reject"
- }
- },
- "type": "object",
- "required": [
- "assistant_id"
- ],
- "title": "RunCreate",
- "description": "Payload for creating a run."
- },
- "RunCancel": {
- "properties": {
- "wait": {
- "type": "boolean",
- "title": "Wait"
- }
- },
- "type": "object",
- "required": [
- "wait"
- ],
- "title": "RunCancel",
- "description": "Payload for cancelling a run."
- },
- "RunWaitOutput": {
- "type": "object",
- "title": "RunWaitOutput"
- },
- "ErrorResponse": {
- "title": "ErrorResponse",
- "description": "Response body for an error.",
- "type": "object",
- "properties": {
- "detail": {
- "type": "string",
- "title": "Detail",
- "description": "Detail of the error."
- }
- },
- "required": [
- "detail"
- ]
- }
- }
- }
-}
\ No newline at end of file
diff --git a/docs/docs/cloud/reference/api/open_agent_api_ref.html b/docs/docs/cloud/reference/api/open_agent_api_ref.html
deleted file mode 100644
index 3cec4ca09..000000000
--- a/docs/docs/cloud/reference/api/open_agent_api_ref.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
- Open Assistants API Specification
-
-
-
-
-
-
-
-
-
diff --git a/docs/docs/cloud/reference/api/openapi.json b/docs/docs/cloud/reference/api/openapi.json
index c16f2488d..3f2c20c9b 100644
--- a/docs/docs/cloud/reference/api/openapi.json
+++ b/docs/docs/cloud/reference/api/openapi.json
@@ -1557,8 +1557,11 @@
"200": {
"description": "Success",
"content": {
- "application/json": {
- "schema": {}
+ "text/event-stream": {
+ "schema": {
+ "type": "string",
+ "description": "The server will send a stream of events in SSE format.\n\n**Example event**:\n\nid: 1\n\nevent: message\n\ndata: {}"
+ }
}
}
},
@@ -1905,8 +1908,11 @@
"200": {
"description": "Success",
"content": {
- "application/json": {
- "schema": {}
+ "text/event-stream": {
+ "schema": {
+ "type": "string",
+ "description": "The server will send a stream of events in SSE format.\n\n**Example event**:\n\nid: 1\n\nevent: message\n\ndata: {}"
+ }
}
}
},
@@ -2143,8 +2149,11 @@
"200": {
"description": "Success",
"content": {
- "application/json": {
- "schema": {}
+ "text/event-stream": {
+ "schema": {
+ "type": "string",
+ "description": "The server will send a stream of events in SSE format.\n\n**Example event**:\n\nid: 1\n\nevent: message\n\ndata: {}"
+ }
}
}
},
From 62a36befd5a6abd574a2872fefd9d041688fe58e Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 27 Nov 2024 14:53:24 -0800
Subject: [PATCH 064/149] Add in-mem vector search (#2547)
---
.../langgraph/store/duckdb/base.py | 19 +-
.../langgraph/checkpoint/postgres/__init__.py | 16 +-
.../langgraph/checkpoint/postgres/aio.py | 23 +-
.../langgraph/store/postgres/aio.py | 11 +-
.../langgraph/store/postgres/base.py | 45 +-
libs/checkpoint/Makefile | 6 +-
.../langgraph/store/base/__init__.py | 653 +++++++++++++++---
libs/checkpoint/langgraph/store/base/batch.py | 17 +-
libs/checkpoint/langgraph/store/base/embed.py | 380 ++++++++++
.../langgraph/store/memory/__init__.py | 472 +++++++++++--
libs/checkpoint/pyproject.toml | 2 +-
libs/checkpoint/tests/embed_test_utils.py | 55 ++
libs/checkpoint/tests/test_store.py | 539 ++++++++++++++-
13 files changed, 2047 insertions(+), 191 deletions(-)
create mode 100644 libs/checkpoint/langgraph/store/base/embed.py
create mode 100644 libs/checkpoint/tests/embed_test_utils.py
diff --git a/libs/checkpoint-duckdb/langgraph/store/duckdb/base.py b/libs/checkpoint-duckdb/langgraph/store/duckdb/base.py
index e0fb57067..89bf13681 100644
--- a/libs/checkpoint-duckdb/langgraph/store/duckdb/base.py
+++ b/libs/checkpoint-duckdb/langgraph/store/duckdb/base.py
@@ -23,6 +23,7 @@ from langgraph.store.base import (
Op,
PutOp,
Result,
+ SearchItem,
SearchOp,
)
@@ -283,7 +284,7 @@ class DuckDBStore(BaseStore, BaseDuckDBStore[duckdb.DuckDBPyConnection]):
for cur, idx in cursors:
rows = cur.fetchall()
- items = [_row_to_item(_convert_ns(row[0]), row) for row in rows]
+ items = [_row_to_search_item(_convert_ns(row[0]), row) for row in rows]
results[idx] = items
def _batch_list_namespaces_ops(
@@ -376,6 +377,22 @@ def _row_to_item(
)
+def _row_to_search_item(
+ namespace: tuple[str, ...],
+ row: tuple,
+) -> SearchItem:
+ """Convert a row from the database into an SearchItem."""
+ # TODO: Add support for search
+ _, key, val, created_at, updated_at = row
+ return SearchItem(
+ value=val if isinstance(val, dict) else json.loads(val),
+ key=key,
+ namespace=namespace,
+ created_at=created_at,
+ updated_at=updated_at,
+ )
+
+
def _group_ops(ops: Iterable[Op]) -> tuple[dict[type, list[tuple[int, Op]]], int]:
grouped_ops: dict[type, list[tuple[int, Op]]] = defaultdict(list)
tot = 0
diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py
index 2107b05a3..1a3aff119 100644
--- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py
+++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py
@@ -378,15 +378,19 @@ class PostgresSaver(BasePostgresSaver):
# a connection not in pipeline mode can only be used by one
# thread/coroutine at a time, so we acquire a lock
if self.supports_pipeline:
- with self.lock, conn.pipeline(), conn.cursor(
- binary=True, row_factory=dict_row
- ) as cur:
+ with (
+ self.lock,
+ conn.pipeline(),
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
+ ):
yield cur
else:
# Use connection's transaction context manager when pipeline mode not supported
- with self.lock, conn.transaction(), conn.cursor(
- binary=True, row_factory=dict_row
- ) as cur:
+ with (
+ self.lock,
+ conn.transaction(),
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
+ ):
yield cur
else:
with self.lock, conn.cursor(binary=True, row_factory=dict_row) as cur:
diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
index 589520efc..5b07e0067 100644
--- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
+++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
@@ -338,20 +338,25 @@ class AsyncPostgresSaver(BasePostgresSaver):
# a connection not in pipeline mode can only be used by one
# thread/coroutine at a time, so we acquire a lock
if self.supports_pipeline:
- async with self.lock, conn.pipeline(), conn.cursor(
- binary=True, row_factory=dict_row
- ) as cur:
+ async with (
+ self.lock,
+ conn.pipeline(),
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
+ ):
yield cur
else:
# Use connection's transaction context manager when pipeline mode not supported
- async with self.lock, conn.transaction(), conn.cursor(
- binary=True, row_factory=dict_row
- ) as cur:
+ async with (
+ self.lock,
+ conn.transaction(),
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
+ ):
yield cur
else:
- async with self.lock, conn.cursor(
- binary=True, row_factory=dict_row
- ) as cur:
+ async with (
+ self.lock,
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
+ ):
yield cur
def list(
diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/aio.py b/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
index 578523052..b90a9a0d5 100644
--- a/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
+++ b/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
@@ -28,6 +28,7 @@ from langgraph.store.postgres.base import (
_decode_ns_bytes,
_group_ops,
_row_to_item,
+ _row_to_search_item,
)
logger = logging.getLogger(__name__)
@@ -146,7 +147,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
await cur.execute(query, params)
rows = cast(list[Row], await cur.fetchall())
items = [
- _row_to_item(
+ _row_to_search_item(
_decode_ns_bytes(row["prefix"]), row, loader=self._deserializer
)
for row in rows
@@ -195,9 +196,11 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
async with self.lock, conn.pipeline(), conn.cursor(binary=True) as cur:
yield cur
else:
- async with self.lock, conn.transaction(), conn.cursor(
- binary=True
- ) as cur:
+ async with (
+ self.lock,
+ conn.transaction(),
+ conn.cursor(binary=True) as cur,
+ ):
yield cur
else:
async with conn.cursor(binary=True) as cur:
diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/base.py b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
index 3bb343b0c..200218c0d 100644
--- a/libs/checkpoint-postgres/langgraph/store/postgres/base.py
+++ b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
@@ -36,6 +36,7 @@ from langgraph.store.base import (
Op,
PutOp,
Result,
+ SearchItem,
SearchOp,
)
@@ -344,14 +345,18 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
# a connection not in pipeline mode can only be used by one
# thread/coroutine at a time, so we acquire a lock
if self.supports_pipeline:
- with self.lock, conn.pipeline(), conn.cursor(
- binary=True, row_factory=dict_row
- ) as cur:
+ with (
+ self.lock,
+ conn.pipeline(),
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
+ ):
yield cur
else:
- with self.lock, conn.transaction(), conn.cursor(
- binary=True, row_factory=dict_row
- ) as cur:
+ with (
+ self.lock,
+ conn.transaction(),
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
+ ):
yield cur
else:
with conn.cursor(binary=True, row_factory=dict_row) as cur:
@@ -430,7 +435,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
cur.execute(query, params)
rows = cast(list[Row], cur.fetchall())
results[idx] = [
- _row_to_item(
+ _row_to_search_item(
_decode_ns_bytes(row["prefix"]), row, loader=self._deserializer
)
for row in rows
@@ -517,6 +522,32 @@ def _row_to_item(
)
+def _row_to_search_item(
+ namespace: tuple[str, ...],
+ row: Row,
+ *,
+ loader: Optional[Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]] = None,
+) -> SearchItem:
+ """Convert a row from the database into an Item."""
+ loader = loader or _json_loads
+ val = row["value"]
+ score = row.get("score")
+ if score is not None:
+ try:
+ score = float(score) # type: ignore[arg-type]
+ except ValueError:
+ logger.warning("Invalid score: %s", score)
+ score = None
+ return SearchItem(
+ value=val if isinstance(val, dict) else loader(val),
+ key=row["key"],
+ namespace=namespace,
+ created_at=row["created_at"],
+ updated_at=row["updated_at"],
+ score=score,
+ )
+
+
def _group_ops(ops: Iterable[Op]) -> tuple[dict[type, list[tuple[int, Op]]], int]:
grouped_ops: dict[type, list[tuple[int, Op]]] = defaultdict(list)
tot = 0
diff --git a/libs/checkpoint/Makefile b/libs/checkpoint/Makefile
index ddf087ef5..2636db8fe 100644
--- a/libs/checkpoint/Makefile
+++ b/libs/checkpoint/Makefile
@@ -4,11 +4,13 @@
# TESTING AND COVERAGE
######################
+TEST ?= .
+
test:
- poetry run pytest tests
+ poetry run pytest $(TEST)
test_watch:
- poetry run ptw .
+ poetry run ptw $(TEST)
######################
# LINTING AND FORMATTING
diff --git a/libs/checkpoint/langgraph/store/base/__init__.py b/libs/checkpoint/langgraph/store/base/__init__.py
index 098462339..033cfba12 100644
--- a/libs/checkpoint/langgraph/store/base/__init__.py
+++ b/libs/checkpoint/langgraph/store/base/__init__.py
@@ -1,12 +1,27 @@
"""Base classes and types for persistent key-value stores.
-Stores enable persistence and memory that can be shared across threads,
-scoped to user IDs, assistant IDs, or other arbitrary namespaces.
+Stores provide long-term memory that persists across threads and conversations.
+Supports hierarchical namespaces, key-value storage, and optional vector search.
+
+Core types:
+- BaseStore: Store interface with sync/async operations
+- Item: Stored key-value pairs with metadata
+- Op: Get/Put/Search/List operations
"""
from abc import ABC, abstractmethod
from datetime import datetime
-from typing import Any, Iterable, Literal, NamedTuple, Optional, Union, cast
+from typing import Any, Iterable, Literal, NamedTuple, Optional, TypedDict, Union, cast
+
+from langchain_core.embeddings import Embeddings
+
+from langgraph.store.base.embed import (
+ AEmbeddingsFunc,
+ EmbeddingsFunc,
+ ensure_embeddings,
+ get_text_at_path,
+ tokenize_path,
+)
class Item:
@@ -73,112 +88,415 @@ class Item:
}
+class SearchItem(Item):
+ """Represents a result item with additional response metadata."""
+
+ __slots__ = ("score",)
+
+ def __init__(
+ self,
+ namespace: tuple[str, ...],
+ key: str,
+ value: dict[str, Any],
+ created_at: datetime,
+ updated_at: datetime,
+ score: Optional[float] = None,
+ ) -> None:
+ """Initialize a result item.
+
+ Args:
+ namespace: Hierarchical path to the item.
+ key: Unique identifier within the namespace.
+ value: The stored value.
+ created_at: When the item was first created.
+ updated_at: When the item was last updated.
+ score: Relevance/similarity score if from a ranked operation.
+ """
+ super().__init__(
+ value=value,
+ key=key,
+ namespace=namespace,
+ created_at=created_at,
+ updated_at=updated_at,
+ )
+ self.score = score
+
+ def dict(self) -> dict:
+ result = super().dict()
+ result["score"] = self.score
+ return result
+
+
class GetOp(NamedTuple):
- """Operation to retrieve an item by namespace and key."""
+ """Operation to retrieve a specific item by its namespace and key.
+
+ This operation allows precise retrieval of stored items using their full path
+ (namespace) and unique identifier (key) combination.
+
+ ??? example "Examples"
+
+ Basic item retrieval:
+ ```python
+ GetOp(namespace=("users", "profiles"), key="user123")
+ GetOp(namespace=("cache", "embeddings"), key="doc456")
+ ```
+ """
namespace: tuple[str, ...]
- """Hierarchical path for the item."""
+ """Hierarchical path that uniquely identifies the item's location.
+
+ ??? example "Examples"
+
+ ```python
+ ("users",) # Root level users namespace
+ ("users", "profiles") # Profiles within users namespace
+ ```
+ """
+
key: str
- """Unique identifier within the namespace."""
+ """Unique identifier for the item within its specific namespace.
+
+ ??? example "Examples"
+
+ ```python
+ "user123" # For a user profile
+ "doc456" # For a document
+ ```
+ """
class SearchOp(NamedTuple):
- """Operation to search for items within a namespace prefix."""
+ """Operation to search for items within a specified namespace hierarchy.
+
+ This operation supports both structured filtering and natural language search
+ within a given namespace prefix. It provides pagination through limit and offset
+ parameters.
+
+ Note:
+ Natural language search support depends on your store implementation.
+
+ ??? example "Examples"
+ Search with filters and pagination:
+ ```python
+ SearchOp(
+ namespace_prefix=("documents",),
+ filter={"type": "report", "status": "active"},
+ limit=5,
+ offset=10
+ )
+ ```
+
+ Natural language search:
+ ```python
+ SearchOp(
+ namespace_prefix=("users", "content"),
+ query="technical documentation about APIs",
+ limit=20
+ )
+ ```
+ """
namespace_prefix: tuple[str, ...]
- """Hierarchical path prefix to search within."""
+ """Hierarchical path prefix defining the search scope.
+
+ ??? example "Examples"
+
+ ```python
+ () # Search entire store
+ ("documents",) # Search all documents
+ ("users", "content") # Search within user content
+ ```
+ """
+
filter: Optional[dict[str, Any]] = None
- """Key-value pairs to filter results."""
+ """Key-value pairs for filtering results based on exact matches or comparison operators.
+
+ The filter supports both exact matches and operator-based comparisons.
+
+ Supported Operators:
+ - $eq: Equal to (same as direct value comparison)
+ - $ne: Not equal to
+ - $gt: Greater than
+ - $gte: Greater than or equal to
+ - $lt: Less than
+ - $lte: Less than or equal to
+
+ ??? example "Examples"
+
+ Simple exact match:
+
+ ```python
+ {"status": "active"}
+ ```
+
+ Comparison operators:
+
+ ```python
+ {"score": {"$gt": 4.99}} # Score greater than 4.99
+ ```
+
+ Multiple conditions:
+
+ ```python
+ {
+ "score": {"$gte": 3.0},
+ "color": "red"
+ }
+ ```
+
+ Note:
+ Comparison operator support depends on your store implementation.
+ """
+
limit: int = 10
- """Maximum number of items to return."""
+ """Maximum number of items to return in the search results."""
+
offset: int = 0
- """Number of items to skip before returning results."""
+ """Number of matching items to skip for pagination."""
+ query: Optional[str] = None
+ """Natural language search query for semantic search capabilities.
-class PutOp(NamedTuple):
- """Operation to store, update, or delete an item."""
-
- namespace: tuple[str, ...]
- """Hierarchical path for the item.
-
- Represented as a tuple of strings, allowing for nested categorization.
- For example: ("documents", "user123")
- """
-
- key: str
- """Unique identifier for the document.
-
- Should be distinct within its namespace.
- """
-
- value: Optional[dict[str, Any]]
- """Data to be stored, or None to delete the item.
-
- Schema:
- - Should be a dictionary where:
- - Keys are strings representing field names
- - Values can be of any serializable type
- - If None, it indicates that the item should be deleted
+ ??? example "Examples"
+ - "technical documentation about REST APIs"
+ - "machine learning papers from 2023"
"""
-NameSpacePath = tuple[Union[str, Literal["*"]], ...]
+# Type representing a namespace path that can include wildcards
+NamespacePath = tuple[Union[str, Literal["*"]], ...]
+"""A tuple representing a namespace path that can include wildcards.
+Examples:
+ ("users",) # Exact users namespace
+ ("documents", "*") # Any sub-namespace under documents
+ ("cache", "*", "v1") # Any cache category with v1 version
+"""
+
+# Type for specifying how to match namespaces
NamespaceMatchType = Literal["prefix", "suffix"]
+"""Specifies how to match namespace paths.
+
+Values:
+ "prefix": Match from the start of the namespace
+ "suffix": Match from the end of the namespace
+"""
class MatchCondition(NamedTuple):
- """Represents a single match condition."""
+ """Represents a pattern for matching namespaces in the store.
+
+ This class combines a match type (prefix or suffix) with a namespace path
+ pattern that can include wildcards to flexibly match different namespace
+ hierarchies.
+
+ ??? example "Examples"
+ Prefix matching:
+ ```python
+ MatchCondition(match_type="prefix", path=("users", "profiles"))
+ ```
+
+ Suffix matching with wildcard:
+ ```python
+ MatchCondition(match_type="suffix", path=("cache", "*"))
+ ```
+
+ Simple suffix matching:
+ ```python
+ MatchCondition(match_type="suffix", path=("v1",))
+ ```
+ """
match_type: NamespaceMatchType
- path: NameSpacePath
+ """Type of namespace matching to perform."""
+
+ path: NamespacePath
+ """Namespace path pattern that can include wildcards."""
class ListNamespacesOp(NamedTuple):
- """Operation to list namespaces with optional match conditions."""
+ """Operation to list and filter namespaces in the store.
+
+ This operation allows exploring the organization of data, finding specific
+ collections, and navigating the namespace hierarchy.
+
+ ??? example "Examples"
+
+ List all namespaces under the "documents" path:
+ ```python
+ ListNamespacesOp(
+ match_conditions=(MatchCondition(match_type="prefix", path=("documents",)),),
+ max_depth=2
+ )
+ ```
+
+ List all namespaces that end with "v1":
+ ```python
+ ListNamespacesOp(
+ match_conditions=(MatchCondition(match_type="suffix", path=("v1",)),),
+ limit=50
+ )
+ ```
+
+ """
match_conditions: Optional[tuple[MatchCondition, ...]] = None
- """A tuple of match conditions to apply to namespaces."""
+ """Optional conditions for filtering namespaces.
+
+ ??? example "Examples"
+ All user namespaces:
+ ```python
+ (MatchCondition(match_type="prefix", path=("users",)),)
+ ```
+
+ All namespaces that start with "docs" and end with "draft":
+ ```python
+ (
+ MatchCondition(match_type="prefix", path=("docs",)),
+ MatchCondition(match_type="suffix", path=("draft",))
+ )
+ ```
+ """
max_depth: Optional[int] = None
- """Return namespaces up to this depth in the hierarchy."""
+ """Maximum depth of namespace hierarchy to return.
+
+ Note:
+ Namespaces deeper than this level will be truncated.
+ """
limit: int = 100
"""Maximum number of namespaces to return."""
offset: int = 0
- """Number of namespaces to skip before returning results."""
+ """Number of namespaces to skip for pagination."""
+
+
+class PutOp(NamedTuple):
+ """Operation to store, update, or delete an item in the store.
+
+ This class represents a single operation to modify the store's contents,
+ whether adding new items, updating existing ones, or removing them.
+ """
+
+ namespace: tuple[str, ...]
+ """Hierarchical path that identifies the location of the item.
+
+ The namespace acts as a folder-like structure to organize items.
+ Each element in the tuple represents one level in the hierarchy.
+
+ ??? example "Examples"
+ Root level documents
+ ```python
+ ("documents",)
+ ```
+
+ User-specific documents
+ ```python
+ ("documents", "user123")
+ ```
+
+ Nested cache structure
+ ```python
+ ("cache", "embeddings", "v1")
+ ```
+ """
+
+ key: str
+ """Unique identifier for the item within its namespace.
+
+ The key must be unique within the specific namespace to avoid conflicts.
+ Together with the namespace, it forms a complete path to the item.
+
+ Example:
+ If namespace is ("documents", "user123") and key is "report1",
+ the full path would effectively be "documents/user123/report1"
+ """
+
+ value: Optional[dict[str, Any]]
+ """The data to store, or None to mark the item for deletion.
+
+ The value must be a dictionary with string keys and JSON-serializable values.
+ Setting this to None signals that the item should be deleted.
+
+ Example:
+ {
+ "field1": "string value",
+ "field2": 123,
+ "nested": {"can": "contain", "any": "serializable data"}
+ }
+ """
+
+ index: Optional[Union[Literal[False], list[str]]] = None # type: ignore[assignment]
+ """Controls how the item's fields are indexed for search operations.
+
+ Indexing configuration determines how the item can be found through search:
+ - None (default): Uses the store's default indexing configuration (if provided)
+ - False: Disables indexing for this item
+ - list[str]: Specifies which json path fields to index for search
+
+ The item remains accessible through direct get() operations regardless of indexing.
+ When indexed, fields can be searched using natural language queries through
+ vector similarity search (if supported by the store implementation).
+
+ Path Syntax:
+ - Simple field access: "field"
+ - Nested fields: "parent.child.grandchild"
+ - Array indexing:
+ - Specific index: "array[0]"
+ - Last element: "array[-1]"
+ - All elements (each individually): "array[*]"
+
+ ??? example "Examples"
+ - None - Use store defaults
+ - False - Don't index this item
+ - list[str] - List of fields to index
+
+ ```python
+ [
+ "metadata.title", # Nested field access
+ "chapters[*].content", # Index content from all chapters as separate vectors
+ "authors[0].name", # First author's name
+ "revisions[-1].changes", # Most recent revision's changes
+ "sections[*].paragraphs[*].text", # All text from all paragraphs in all sections
+ "metadata.tags[*]", # All tags in metadata
+ ]
+ ```
+ """
Op = Union[GetOp, SearchOp, PutOp, ListNamespacesOp]
-Result = Union[Item, list[Item], list[tuple[str, ...]], None]
+Result = Union[Item, list[Item], list[SearchItem], list[tuple[str, ...]], None]
class InvalidNamespaceError(ValueError):
"""Provided namespace is invalid."""
-def _validate_namespace(namespace: tuple[str, ...]) -> None:
- if not namespace:
- raise InvalidNamespaceError("Namespace cannot be empty.")
- for label in namespace:
- if not isinstance(label, str):
- raise InvalidNamespaceError(
- f"Invalid namespace label '{label}' found in {namespace}. Namespace labels"
- f" must be strings, but got {type(label).__name__}."
- )
- if "." in label:
- raise InvalidNamespaceError(
- f"Invalid namespace label '{label}' found in {namespace}. Namespace labels cannot contain periods ('.')."
- )
- elif not label:
- raise InvalidNamespaceError(
- f"Namespace labels cannot be empty strings. Got {label} in {namespace}"
- )
- if namespace[0] == "langgraph":
- raise InvalidNamespaceError(
- f'Root label for namespace cannot be "langgraph". Got: {namespace}'
- )
+class IndexConfig(TypedDict, total=False):
+ """Configuration for indexing documents for semantic search in the store."""
+
+ dims: int
+ """Number of dimensions in the embedding vectors.
+
+ Common embedding models have the following dimensions:
+ - OpenAI text-embedding-3-large: 256, 1024, or 3072
+ - OpenAI text-embedding-3-small: 512 or 1536
+ - OpenAI text-embedding-ada-002: 1536
+ - Cohere embed-english-v3.0: 1024
+ - Cohere embed-english-light-v3.0: 384
+ - Cohere embed-multilingual-v3.0: 1024
+ - Cohere embed-multilingual-light-v3.0: 384
+ """
+
+ embed: Union[Embeddings, EmbeddingsFunc, AEmbeddingsFunc]
+ """Optional function to generate embeddings from text."""
+
+ fields: Optional[list[str]]
+ """Fields to extract text from for embedding generation.
+
+ Defaults to the root ["$"], which embeds the json object as a whole.
+ """
class BaseStore(ABC):
@@ -231,14 +549,16 @@ class BaseStore(ABC):
namespace_prefix: tuple[str, ...],
/,
*,
+ query: Optional[str] = None,
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
- ) -> list[Item]:
+ ) -> list[SearchItem]:
"""Search for items within a namespace prefix.
Args:
namespace_prefix: Hierarchical path prefix to search within.
+ query: Optional query for natural language search.
filter: Key-value pairs to filter results.
limit: Maximum number of items to return.
offset: Number of items to skip before returning results.
@@ -246,18 +566,54 @@ class BaseStore(ABC):
Returns:
List of items matching the search criteria.
"""
- return self.batch([SearchOp(namespace_prefix, filter, limit, offset)])[0]
+ return self.batch([SearchOp(namespace_prefix, filter, limit, offset, query)])[0]
- def put(self, namespace: tuple[str, ...], key: str, value: dict[str, Any]) -> None:
- """Store or update an item.
+ def put(
+ self,
+ namespace: tuple[str, ...],
+ key: str,
+ value: dict[str, Any],
+ index: Optional[Union[Literal[False], list[str]]] = None,
+ ) -> None:
+ """Store or update an item in the store.
Args:
- namespace: Hierarchical path for the item.
- key: Unique identifier within the namespace.
- value: Dictionary containing the item's data.
+ namespace: Hierarchical path for the item, represented as a tuple of strings.
+ Example: ("documents", "user123")
+ key: Unique identifier within the namespace. Together with namespace forms
+ the complete path to the item.
+ value: Dictionary containing the item's data. Must contain string keys
+ and JSON-serializable values.
+ index: Controls how the item's fields are indexed for search:
+ - None (default): Use store's default indexing configuration
+ - False: Disable indexing for this item
+ - list[str]: List of field paths to index, supporting:
+ - Nested fields: "metadata.title"
+ - Array access: "chapters[*].content" (each indexed separately)
+ - Specific indices: "authors[0].name"
+
+ Note:
+ Indexing capabilities depend on your store implementation.
+ Some implementations may support only a subset of indexing features.
+
+ ??? example "Examples"
+ Simple storage without special indexing (respects store defaults)
+ ```python
+ store.put(("docs",), "report", {"title": "Annual Report"})
+ ```
+
+ Index specific fields for search
+ ```python
+ store.put(("docs",), "report", {"title": "Annual Report"}, index=["title"])
+ ```
+
+ Do not index for semantic search
+ ```python
+ store.put(("docs",), "report", {"title": "Annual Report"}, index=False)
+ ```
"""
_validate_namespace(namespace)
- self.batch([PutOp(namespace, key, value)])
+ self.batch([PutOp(namespace, key, value, index=index)])
def delete(self, namespace: tuple[str, ...], key: str) -> None:
"""Delete an item.
@@ -271,8 +627,8 @@ class BaseStore(ABC):
def list_namespaces(
self,
*,
- prefix: Optional[NameSpacePath] = None,
- suffix: Optional[NameSpacePath] = None,
+ prefix: Optional[NamespacePath] = None,
+ suffix: Optional[NamespacePath] = None,
max_depth: Optional[int] = None,
limit: int = 100,
offset: int = 0,
@@ -286,7 +642,7 @@ class BaseStore(ABC):
prefix (Optional[Tuple[str, ...]]): Filter namespaces that start with this path.
suffix (Optional[Tuple[str, ...]]): Filter namespaces that end with this path.
max_depth (Optional[int]): Return namespaces up to this depth in the hierarchy.
- Namespaces deeper than this level will be truncated to this depth.
+ Namespaces deeper than this level will be truncated.
limit (int): Maximum number of namespaces to return (default 100).
offset (int): Number of namespaces to skip for pagination (default 0).
@@ -294,16 +650,18 @@ class BaseStore(ABC):
List[Tuple[str, ...]]: A list of namespace tuples that match the criteria.
Each tuple represents a full namespace path up to `max_depth`.
- Examples:
-
+ ??? example "Examples":
Setting max_depth=3. Given the namespaces:
- # ("a", "b", "c")
- # ("a", "b", "d", "e")
- # ("a", "b", "d", "i")
- # ("a", "b", "f")
- # ("a", "c", "f")
- store.list_namespaces(prefix=("a", "b"), max_depth=3)
- # [("a", "b", "c"), ("a", "b", "d"), ("a", "b", "f")]
+ ```python
+ # Example if you have the following namespaces:
+ # ("a", "b", "c")
+ # ("a", "b", "d", "e")
+ # ("a", "b", "d", "i")
+ # ("a", "b", "f")
+ # ("a", "c", "f")
+ store.list_namespaces(prefix=("a", "b"), max_depth=3)
+ # [("a", "b", "c"), ("a", "b", "d"), ("a", "b", "f")]
+ ```
"""
match_conditions = []
if prefix:
@@ -336,14 +694,16 @@ class BaseStore(ABC):
namespace_prefix: tuple[str, ...],
/,
*,
+ query: Optional[str] = None,
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
- ) -> list[Item]:
+ ) -> list[SearchItem]:
"""Asynchronously search for items within a namespace prefix.
Args:
namespace_prefix: Hierarchical path prefix to search within.
+ query: Optional query for natural language search.
filter: Key-value pairs to filter results.
limit: Maximum number of items to return.
offset: Number of items to skip before returning results.
@@ -351,22 +711,61 @@ class BaseStore(ABC):
Returns:
List of items matching the search criteria.
"""
- return (await self.abatch([SearchOp(namespace_prefix, filter, limit, offset)]))[
- 0
- ]
+ return (
+ await self.abatch(
+ [SearchOp(namespace_prefix, filter, limit, offset, query)]
+ )
+ )[0]
async def aput(
- self, namespace: tuple[str, ...], key: str, value: dict[str, Any]
+ self,
+ namespace: tuple[str, ...],
+ key: str,
+ value: dict[str, Any],
+ index: Optional[Union[Literal[False], list[str]]] = None,
) -> None:
- """Asynchronously store or update an item.
+ """Asynchronously store or update an item in the store.
Args:
- namespace: Hierarchical path for the item.
- key: Unique identifier within the namespace.
- value: Dictionary containing the item's data.
+ namespace: Hierarchical path for the item, represented as a tuple of strings.
+ Example: ("documents", "user123")
+ key: Unique identifier within the namespace. Together with namespace forms
+ the complete path to the item.
+ value: Dictionary containing the item's data. Must contain string keys
+ and JSON-serializable values.
+ index: Controls how the item's fields are indexed for search:
+ - None (default): Use store's default indexing configuration
+ - False: Disable indexing for this item
+ - list[str]: List of field paths to index, supporting:
+ - Nested fields: "metadata.title"
+ - Array access: "chapters[*].content" (each indexed separately)
+ - Specific indices: "authors[0].name"
+
+ Note:
+ Indexing capabilities depend on your store implementation.
+ Some implementations may support only a subset of indexing features.
+
+ ??? example "Examples"
+ Simple storage without special indexing:
+ ```python
+ await store.aput(("docs",), "report", {"title": "Annual Report"})
+ ```
+
+ Index specific fields for search:
+ ```python
+ await store.aput(
+ ("docs",),
+ "report",
+ {
+ "title": "Q4 Report",
+ "chapters": [{"content": "..."}, {"content": "..."}]
+ },
+ index=["title", "chapters[*].content"]
+ )
+ ```
"""
_validate_namespace(namespace)
- await self.abatch([PutOp(namespace, key, value)])
+ await self.abatch([PutOp(namespace, key, value, index=index)])
async def adelete(self, namespace: tuple[str, ...], key: str) -> None:
"""Asynchronously delete an item.
@@ -380,8 +779,8 @@ class BaseStore(ABC):
async def alist_namespaces(
self,
*,
- prefix: Optional[NameSpacePath] = None,
- suffix: Optional[NameSpacePath] = None,
+ prefix: Optional[NamespacePath] = None,
+ suffix: Optional[NamespacePath] = None,
max_depth: Optional[int] = None,
limit: int = 100,
offset: int = 0,
@@ -403,16 +802,19 @@ class BaseStore(ABC):
List[Tuple[str, ...]]: A list of namespace tuples that match the criteria.
Each tuple represents a full namespace path up to `max_depth`.
- Examples:
+ ??? example "Examples"
+ Setting max_depth=3 with existing namespaces:
+ ```python
+ # Given the following namespaces:
+ # ("a", "b", "c")
+ # ("a", "b", "d", "e")
+ # ("a", "b", "d", "i")
+ # ("a", "b", "f")
+ # ("a", "c", "f")
- Setting max_depth=3. Given the namespaces:
- # ("a", "b", "c")
- # ("a", "b", "d", "e")
- # ("a", "b", "d", "i")
- # ("a", "b", "f")
- # ("a", "c", "f")
- await store.alist_namespaces(prefix=("a", "b"), max_depth=3)
- # [("a", "b", "c"), ("a", "b", "d"), ("a", "b", "f")]
+ await store.alist_namespaces(prefix=("a", "b"), max_depth=3)
+ # Returns: [("a", "b", "c"), ("a", "b", "d"), ("a", "b", "f")]
+ ```
"""
match_conditions = []
if prefix:
@@ -427,3 +829,44 @@ class BaseStore(ABC):
offset=offset,
)
return (await self.abatch([op]))[0]
+
+
+def _validate_namespace(namespace: tuple[str, ...]) -> None:
+ if not namespace:
+ raise InvalidNamespaceError("Namespace cannot be empty.")
+ for label in namespace:
+ if not isinstance(label, str):
+ raise InvalidNamespaceError(
+ f"Invalid namespace label '{label}' found in {namespace}. Namespace labels"
+ f" must be strings, but got {type(label).__name__}."
+ )
+ if "." in label:
+ raise InvalidNamespaceError(
+ f"Invalid namespace label '{label}' found in {namespace}. Namespace labels cannot contain periods ('.')."
+ )
+ elif not label:
+ raise InvalidNamespaceError(
+ f"Namespace labels cannot be empty strings. Got {label} in {namespace}"
+ )
+ if namespace[0] == "langgraph":
+ raise InvalidNamespaceError(
+ f'Root label for namespace cannot be "langgraph". Got: {namespace}'
+ )
+
+
+__all__ = [
+ "BaseStore",
+ "Item",
+ "Op",
+ "PutOp",
+ "GetOp",
+ "SearchOp",
+ "ListNamespacesOp",
+ "MatchCondition",
+ "NamespacePath",
+ "NamespaceMatchType",
+ "Embeddings",
+ "ensure_embeddings",
+ "tokenize_path",
+ "get_text_at_path",
+]
diff --git a/libs/checkpoint/langgraph/store/base/batch.py b/libs/checkpoint/langgraph/store/base/batch.py
index b1030942d..33c502574 100644
--- a/libs/checkpoint/langgraph/store/base/batch.py
+++ b/libs/checkpoint/langgraph/store/base/batch.py
@@ -1,6 +1,6 @@
import asyncio
import weakref
-from typing import Any, Optional
+from typing import Any, Literal, Optional, Union
from langgraph.store.base import (
BaseStore,
@@ -8,9 +8,10 @@ from langgraph.store.base import (
Item,
ListNamespacesOp,
MatchCondition,
- NameSpacePath,
+ NamespacePath,
Op,
PutOp,
+ SearchItem,
SearchOp,
_validate_namespace,
)
@@ -43,12 +44,13 @@ class AsyncBatchedBaseStore(BaseStore):
namespace_prefix: tuple[str, ...],
/,
*,
+ query: Optional[str] = None,
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
- ) -> list[Item]:
+ ) -> list[SearchItem]:
fut = self._loop.create_future()
- self._aqueue[fut] = SearchOp(namespace_prefix, filter, limit, offset)
+ self._aqueue[fut] = SearchOp(namespace_prefix, filter, limit, offset, query)
return await fut
async def aput(
@@ -56,10 +58,11 @@ class AsyncBatchedBaseStore(BaseStore):
namespace: tuple[str, ...],
key: str,
value: dict[str, Any],
+ index: Optional[Union[Literal[False], list[str]]] = None,
) -> None:
_validate_namespace(namespace)
fut = self._loop.create_future()
- self._aqueue[fut] = PutOp(namespace, key, value)
+ self._aqueue[fut] = PutOp(namespace, key, value, index)
return await fut
async def adelete(
@@ -74,8 +77,8 @@ class AsyncBatchedBaseStore(BaseStore):
async def alist_namespaces(
self,
*,
- prefix: Optional[NameSpacePath] = None,
- suffix: Optional[NameSpacePath] = None,
+ prefix: Optional[NamespacePath] = None,
+ suffix: Optional[NamespacePath] = None,
max_depth: Optional[int] = None,
limit: int = 100,
offset: int = 0,
diff --git a/libs/checkpoint/langgraph/store/base/embed.py b/libs/checkpoint/langgraph/store/base/embed.py
new file mode 100644
index 000000000..0434481cd
--- /dev/null
+++ b/libs/checkpoint/langgraph/store/base/embed.py
@@ -0,0 +1,380 @@
+"""Utilities for working with embedding functions and LangChain's Embeddings interface.
+
+This module provides tools to wrap arbitrary embedding functions (both sync and async)
+into LangChain's Embeddings interface. This enables using custom embedding functions
+with LangChain-compatible tools while maintaining support for both synchronous and
+asynchronous operations.
+"""
+
+import asyncio
+import json
+from typing import Any, Awaitable, Callable, Optional, Sequence, Union
+
+from langchain_core.embeddings import Embeddings
+
+EmbeddingsFunc = Callable[[Sequence[str]], list[list[float]]]
+"""Type for synchronous embedding functions.
+
+The function should take a sequence of strings and return a list of embeddings,
+where each embedding is a list of floats. The dimensionality of the embeddings
+should be consistent for all inputs.
+"""
+
+AEmbeddingsFunc = Callable[[Sequence[str]], Awaitable[list[list[float]]]]
+"""Type for asynchronous embedding functions.
+
+Similar to EmbeddingsFunc, but returns an awaitable that resolves to the embeddings.
+"""
+
+
+def ensure_embeddings(
+ embed: Union[Embeddings, EmbeddingsFunc, AEmbeddingsFunc, None],
+) -> Embeddings:
+ """Ensure that an embedding function conforms to LangChain's Embeddings interface.
+
+ This function wraps arbitrary embedding functions to make them compatible with
+ LangChain's Embeddings interface. It handles both synchronous and asynchronous
+ functions.
+
+ Args:
+ embed: Either an existing Embeddings instance, or a function that converts
+ text to embeddings. If the function is async, it will be used for both
+ sync and async operations.
+
+ Returns:
+ An Embeddings instance that wraps the provided function(s).
+
+ ??? example "Examples"
+ Wrap a synchronous embedding function:
+ ```python
+ def my_embed_fn(texts):
+ return [[0.1, 0.2] for _ in texts]
+
+ embeddings = ensure_embeddings(my_embed_fn)
+ result = embeddings.embed_query("hello") # Returns [0.1, 0.2]
+ ```
+
+ Wrap an asynchronous embedding function:
+ ```python
+ async def my_async_fn(texts):
+ return [[0.1, 0.2] for _ in texts]
+
+ embeddings = ensure_embeddings(my_async_fn)
+ result = await embeddings.aembed_query("hello") # Returns [0.1, 0.2]
+ ```
+ """
+ if embed is None:
+ raise ValueError("embed must be provided")
+ if isinstance(embed, Embeddings):
+ return embed
+ return EmbeddingsLambda(embed)
+
+
+class EmbeddingsLambda(Embeddings):
+ """Wrapper to convert embedding functions into LangChain's Embeddings interface.
+
+ This class allows arbitrary embedding functions to be used with LangChain-compatible
+ tools. It supports both synchronous and asynchronous operations, and can handle:
+ 1. A synchronous function for sync operations (async operations will use sync function)
+ 2. An async function for both sync/async operations (sync operations will raise an error)
+
+ The embedding functions should convert text into fixed-dimensional vectors that
+ capture the semantic meaning of the text.
+
+ Args:
+ func: Function that converts text to embeddings. Can be sync or async.
+ If async, it will be used for async operations, but sync operations
+ will raise an error. If sync, it will be used for both sync and async operations.
+
+ ??? example "Examples"
+ With a sync function:
+ ```python
+ def my_embed_fn(texts):
+ # Return 2D embeddings for each text
+ return [[0.1, 0.2] for _ in texts]
+
+ embeddings = EmbeddingsLambda(my_embed_fn)
+ result = embeddings.embed_query("hello") # Returns [0.1, 0.2]
+ await embeddings.aembed_query("hello") # Also returns [0.1, 0.2]
+ ```
+
+ With an async function:
+ ```python
+ async def my_async_fn(texts):
+ return [[0.1, 0.2] for _ in texts]
+
+ embeddings = EmbeddingsLambda(my_async_fn)
+ await embeddings.aembed_query("hello") # Returns [0.1, 0.2]
+ # Note: embed_query() would raise an error
+ ```
+ """
+
+ def __init__(
+ self,
+ func: Union[EmbeddingsFunc, AEmbeddingsFunc],
+ ) -> None:
+ if func is None:
+ raise ValueError("func must be provided")
+ if _is_async_callable(func):
+ self.afunc = func
+ else:
+ self.func = func
+
+ def embed_documents(self, texts: list[str]) -> list[list[float]]:
+ """Embed a list of texts into vectors.
+
+ Args:
+ texts: list of texts to convert to embeddings.
+
+ Returns:
+ list of embeddings, one per input text. Each embedding is a list of floats.
+
+ Raises:
+ ValueError: If the instance was initialized with only an async function.
+ """
+ func = getattr(self, "func", None)
+ if func is None:
+ raise ValueError(
+ "EmbeddingsLambda was initialized with an async function but no sync function. "
+ "Use aembed_documents for async operation or provide a sync function."
+ )
+ return func(texts)
+
+ def embed_query(self, text: str) -> list[float]:
+ """Embed a single piece of text.
+
+ Args:
+ text: Text to convert to an embedding.
+
+ Returns:
+ Embedding vector as a list of floats.
+
+ Note:
+ This is equivalent to calling embed_documents with a single text
+ and taking the first result.
+ """
+ return self.embed_documents([text])[0]
+
+ async def aembed_documents(self, texts: list[str]) -> list[list[float]]:
+ """Asynchronously embed a list of texts into vectors.
+
+ Args:
+ texts: list of texts to convert to embeddings.
+
+ Returns:
+ list of embeddings, one per input text. Each embedding is a list of floats.
+
+ Note:
+ If no async function was provided, this falls back to the sync implementation.
+ """
+ afunc = getattr(self, "afunc", None)
+ if afunc is None:
+ return await super().aembed_documents(texts)
+ return await afunc(texts)
+
+ async def aembed_query(self, text: str) -> list[float]:
+ """Asynchronously embed a single piece of text.
+
+ Args:
+ text: Text to convert to an embedding.
+
+ Returns:
+ Embedding vector as a list of floats.
+
+ Note:
+ This is equivalent to calling aembed_documents with a single text
+ and taking the first result.
+ """
+ afunc = getattr(self, "afunc", None)
+ if afunc is None:
+ return await super().aembed_query(text)
+ return (await afunc([text]))[0]
+
+
+def get_text_at_path(obj: Any, path: Union[str, list[str]]) -> list[str]:
+ """Extract text from an object using a path expression or pre-tokenized path.
+
+ Args:
+ obj: The object to extract text from
+ path: Either a path string or pre-tokenized path list.
+
+ !!! info "Path types handled"
+ - Simple paths: "field1.field2"
+ - Array indexing: "[0]", "[*]", "[-1]"
+ - Wildcards: "*"
+ - Multi-field selection: "{field1,field2}"
+ - Nested paths in multi-field: "{field1,nested.field2}"
+ """
+ if not path or path == "$":
+ return [json.dumps(obj, sort_keys=True)]
+
+ tokens = tokenize_path(path) if isinstance(path, str) else path
+
+ def _extract_from_obj(obj: Any, tokens: list[str], pos: int) -> list[str]:
+ if pos >= len(tokens):
+ if isinstance(obj, (str, int, float, bool)):
+ return [str(obj)]
+ elif obj is None:
+ return []
+ elif isinstance(obj, (list, dict)):
+ return [json.dumps(obj, sort_keys=True)]
+ return []
+
+ token = tokens[pos]
+ results = []
+
+ if token.startswith("[") and token.endswith("]"):
+ if not isinstance(obj, list):
+ return []
+
+ index = token[1:-1]
+ if index == "*":
+ for item in obj:
+ results.extend(_extract_from_obj(item, tokens, pos + 1))
+ else:
+ try:
+ idx = int(index)
+ if idx < 0:
+ idx = len(obj) + idx
+ if 0 <= idx < len(obj):
+ results.extend(_extract_from_obj(obj[idx], tokens, pos + 1))
+ except (ValueError, IndexError):
+ return []
+
+ elif token.startswith("{") and token.endswith("}"):
+ if not isinstance(obj, dict):
+ return []
+
+ fields = [f.strip() for f in token[1:-1].split(",")]
+ for field in fields:
+ nested_tokens = tokenize_path(field)
+ if nested_tokens:
+ current_obj: Optional[dict] = obj
+ for nested_token in nested_tokens:
+ if (
+ isinstance(current_obj, dict)
+ and nested_token in current_obj
+ ):
+ current_obj = current_obj[nested_token]
+ else:
+ current_obj = None
+ break
+ if current_obj is not None:
+ if isinstance(current_obj, (str, int, float, bool)):
+ results.append(str(current_obj))
+ elif isinstance(current_obj, (list, dict)):
+ results.append(json.dumps(current_obj, sort_keys=True))
+
+ # Handle wildcard
+ elif token == "*":
+ if isinstance(obj, dict):
+ for value in obj.values():
+ results.extend(_extract_from_obj(value, tokens, pos + 1))
+ elif isinstance(obj, list):
+ for item in obj:
+ results.extend(_extract_from_obj(item, tokens, pos + 1))
+
+ # Handle regular field
+ else:
+ if isinstance(obj, dict) and token in obj:
+ results.extend(_extract_from_obj(obj[token], tokens, pos + 1))
+
+ return results
+
+ return _extract_from_obj(obj, tokens, 0)
+
+
+# Private utility functions
+
+
+def tokenize_path(path: str) -> list[str]:
+ """Tokenize a path into components.
+
+ !!! info "Types handled"
+ - Simple paths: "field1.field2"
+ - Array indexing: "[0]", "[*]", "[-1]"
+ - Wildcards: "*"
+ - Multi-field selection: "{field1,field2}"
+ """
+ if not path:
+ return []
+
+ tokens = []
+ current: list[str] = []
+ i = 0
+ while i < len(path):
+ char = path[i]
+
+ if char == "[": # Handle array index
+ if current:
+ tokens.append("".join(current))
+ current = []
+ bracket_count = 1
+ index_chars = ["["]
+ i += 1
+ while i < len(path) and bracket_count > 0:
+ if path[i] == "[":
+ bracket_count += 1
+ elif path[i] == "]":
+ bracket_count -= 1
+ index_chars.append(path[i])
+ i += 1
+ tokens.append("".join(index_chars))
+ continue
+
+ elif char == "{": # Handle multi-field selection
+ if current:
+ tokens.append("".join(current))
+ current = []
+ brace_count = 1
+ field_chars = ["{"]
+ i += 1
+ while i < len(path) and brace_count > 0:
+ if path[i] == "{":
+ brace_count += 1
+ elif path[i] == "}":
+ brace_count -= 1
+ field_chars.append(path[i])
+ i += 1
+ tokens.append("".join(field_chars))
+ continue
+
+ elif char == ".": # Handle regular field
+ if current:
+ tokens.append("".join(current))
+ current = []
+ else:
+ current.append(char)
+ i += 1
+
+ if current:
+ tokens.append("".join(current))
+
+ return tokens
+
+
+def _is_async_callable(
+ func: Any,
+) -> bool:
+ """Check if a function is async.
+
+ This includes both async def functions and classes with async __call__ methods.
+
+ Args:
+ func: Function or callable object to check.
+
+ Returns:
+ True if the function is async, False otherwise.
+ """
+ return (
+ asyncio.iscoroutinefunction(func)
+ or hasattr(func, "__call__") # noqa: B004
+ and asyncio.iscoroutinefunction(func.__call__)
+ )
+
+
+__all__ = [
+ "ensure_embeddings",
+ "EmbeddingsFunc",
+ "AEmbeddingsFunc",
+]
diff --git a/libs/checkpoint/langgraph/store/memory/__init__.py b/libs/checkpoint/langgraph/store/memory/__init__.py
index 69a315096..40011a7ba 100644
--- a/libs/checkpoint/langgraph/store/memory/__init__.py
+++ b/libs/checkpoint/langgraph/store/memory/__init__.py
@@ -1,79 +1,379 @@
+"""In-memory key-value store.
+
+A lightweight store implementation using Python dictionaries. Supports basic
+key-value operations and vector search when configured with embeddings.
+
+Examples:
+ Basic key-value storage:
+ store = InMemoryStore()
+ store.put(("users", "123"), "prefs", {"theme": "dark"})
+ item = store.get(("users", "123"), "prefs")
+
+ Vector search with embeddings:
+ from langchain_openai import OpenAIEmbeddings
+ store = InMemoryStore(index={
+ "dims": 1536,
+ "embed": OpenAIEmbeddings(model="text-embedding-3-small"),
+ })
+
+ # Store documents
+ store.put(("docs",), "doc1", {"text": "Python tutorial"})
+ store.put(("docs",), "doc2", {"text": "TypeScript guide"})
+
+ # Search by similarity
+ results = store.search(("docs",), query="python programming")
+
+
+Note:
+ For production use cases requiring persistence, use a database-backed store instead.
+"""
+
+import asyncio
+import concurrent.futures as cf
+import functools
+import logging
from collections import defaultdict
from datetime import datetime, timezone
-from typing import Iterable
+from importlib import util
+from typing import Any, Iterable, Optional
+
+from langchain_core.embeddings import Embeddings
from langgraph.store.base import (
BaseStore,
GetOp,
+ IndexConfig,
Item,
ListNamespacesOp,
MatchCondition,
Op,
PutOp,
Result,
+ SearchItem,
SearchOp,
+ ensure_embeddings,
+ get_text_at_path,
+ tokenize_path,
)
+logger = logging.getLogger(__name__)
+
class InMemoryStore(BaseStore):
- """A KV store backed by an in-memory python dictionary.
+ """In-memory dictionary-backed store with optional vector search.
- Useful for testing/experimentation and lightweight PoC's.
- For actual persistence, use a Store backed by a proper database.
+ Examples:
+ Basic key-value storage:
+ store = InMemoryStore()
+ store.put(("users", "123"), "prefs", {"theme": "dark"})
+ item = store.get(("users", "123"), "prefs")
+
+ Vector search with embeddings:
+ from langchain_openai import OpenAIEmbeddings
+ store = InMemoryStore(index={
+ "dims": 1536,
+ "embed": OpenAIEmbeddings(model="text-embedding-3-small"),
+ })
+
+ # Store documents
+ store.put(("docs",), "doc1", {"text": "Python tutorial"})
+ store.put(("docs",), "doc2", {"text": "TypeScript guide"})
+
+ # Search by similarity
+ results = store.search(("docs",), query="python programming")
+
+ Warning:
+ This store keeps all data in memory. Data is lost when the process exits.
+ For persistence, use a database-backed store like PostgresStore.
+
+ Tip:
+ For vector search, install numpy for better performance:
+ ```bash
+ pip install numpy
+ ```
"""
- __slots__ = ("_data",)
+ __slots__ = (
+ "_data",
+ "_vectors",
+ "index_config",
+ "embeddings",
+ )
- def __init__(self) -> None:
+ def __init__(self, *, index: Optional[IndexConfig] = None) -> None:
+ # Both _data and _vectors are wrapped in the In-memory API
+ # Do not change their names
self._data: dict[tuple[str, ...], dict[str, Item]] = defaultdict(dict)
+ # [ns][key][path]
+ self._vectors: dict[tuple[str, ...], dict[str, dict[str, list[float]]]] = (
+ defaultdict(lambda: defaultdict(dict))
+ )
+ self.index_config = index
+ if self.index_config:
+ self.index_config = self.index_config.copy()
+ self.embeddings: Optional[Embeddings] = ensure_embeddings(
+ self.index_config.get("embed"),
+ )
+ self.index_config["__tokenized_fields"] = [
+ (p, tokenize_path(p)) if p != "$" else (p, p)
+ for p in (self.index_config.get("fields") or ["$"])
+ ]
+
+ else:
+ self.index_config = None
+ self.embeddings = None
def batch(self, ops: Iterable[Op]) -> list[Result]:
+ # The batch/abatch methods are treated as internal.
+ # Users should access via put/search/get/list_namespaces/etc.
+ results, put_ops, search_ops = self._prepare_ops(ops)
+ if search_ops:
+ queryinmem_store = self._embed_search_queries(search_ops)
+ self._batch_search(search_ops, queryinmem_store, results)
+
+ to_embed = self._extract_texts(put_ops)
+ if to_embed and self.index_config and self.embeddings:
+ embeddings = self.embeddings.embed_documents(list(to_embed))
+ self._insertinmem_store(to_embed, embeddings)
+ self._apply_put_ops(put_ops)
+ return results
+
+ async def abatch(self, ops: Iterable[Op]) -> list[Result]:
+ # The batch/abatch methods are treated as internal.
+ # Users should access via put/search/get/list_namespaces/etc.
+ results, put_ops, search_ops = self._prepare_ops(ops)
+ if search_ops:
+ queryinmem_store = await self._aembed_search_queries(search_ops)
+ self._batch_search(search_ops, queryinmem_store, results)
+
+ to_embed = self._extract_texts(put_ops)
+ if to_embed and self.index_config and self.embeddings:
+ embeddings = await self.embeddings.aembed_documents(list(to_embed))
+ self._insertinmem_store(to_embed, embeddings)
+ self._apply_put_ops(put_ops)
+ return results
+
+ # Helpers
+
+ def _filter_items(self, op: SearchOp) -> list[tuple[Item, list[list[float]]]]:
+ """Filter items by namespace and filter function, return items with their embeddings."""
+ namespace_prefix = op.namespace_prefix
+
+ def filter_func(item: Item) -> bool:
+ if not op.filter:
+ return True
+
+ return all(
+ _compare_values(item.value.get(key), filter_value)
+ for key, filter_value in op.filter.items()
+ )
+
+ filtered = []
+ for namespace in self._data:
+ if not (
+ namespace[: len(namespace_prefix)] == namespace_prefix
+ if len(namespace) >= len(namespace_prefix)
+ else False
+ ):
+ continue
+
+ for key, item in self._data[namespace].items():
+ if filter_func(item):
+ if op.query and (embeddings := self._vectors[namespace].get(key)):
+ filtered.append((item, list(embeddings.values())))
+ else:
+ filtered.append((item, []))
+ return filtered
+
+ def _embed_search_queries(
+ self,
+ search_ops: dict[int, tuple[SearchOp, list[tuple[Item, list[list[float]]]]]],
+ ) -> dict[str, list[float]]:
+ queryinmem_store = {}
+ if self.index_config and self.embeddings and search_ops:
+ queries = {op.query for (op, _) in search_ops.values() if op.query}
+
+ if queries:
+ with cf.ThreadPoolExecutor() as executor:
+ futures = {
+ q: executor.submit(self.embeddings.embed_query, q)
+ for q in list(queries)
+ }
+ for query, future in futures.items():
+ queryinmem_store[query] = future.result()
+
+ return queryinmem_store
+
+ async def _aembed_search_queries(
+ self,
+ search_ops: dict[int, tuple[SearchOp, list[tuple[Item, list[list[float]]]]]],
+ ) -> dict[str, list[float]]:
+ queryinmem_store = {}
+ if self.index_config and self.embeddings and search_ops:
+ queries = {op.query for (op, _) in search_ops.values() if op.query}
+
+ if queries:
+ coros = [self.embeddings.aembed_query(q) for q in list(queries)]
+ results = await asyncio.gather(*coros)
+ queryinmem_store = dict(zip(queries, results))
+
+ return queryinmem_store
+
+ def _batch_search(
+ self,
+ ops: dict[int, tuple[SearchOp, list[tuple[Item, list[list[float]]]]]],
+ queryinmem_store: dict[str, list[float]],
+ results: list[Result],
+ ) -> None:
+ """Perform batch similarity search for multiple queries."""
+ for i, (op, candidates) in ops.items():
+ if not candidates:
+ results[i] = []
+ continue
+ if op.query and queryinmem_store:
+ query_embedding = queryinmem_store[op.query]
+ flat_items, flat_vectors = [], []
+ scoreless = []
+ for item, vectors in candidates:
+ for vector in vectors:
+ flat_items.append(item)
+ flat_vectors.append(vector)
+ if not vectors:
+ scoreless.append(item)
+
+ scores = _cosine_similarity(query_embedding, flat_vectors)
+ sorted_results = sorted(
+ zip(scores, flat_items), key=lambda x: x[0], reverse=True
+ )
+ # max pooling
+ seen: set[tuple[tuple[str, ...], str]] = set()
+ kept: list[tuple[Optional[float], Item]] = []
+ for score, item in sorted_results:
+ key = (item.namespace, item.key)
+ if key in seen:
+ continue
+ ix = len(seen)
+ seen.add(key)
+ if ix >= op.offset + op.limit:
+ break
+ if ix < op.offset:
+ continue
+
+ kept.append((score, item))
+ if scoreless and len(kept) < op.limit:
+ # Corner case: if we request more items than what we have embedded,
+ # fill the rest with non-scored items
+ kept.extend(
+ (None, item) for item in scoreless[: op.limit - len(kept)]
+ )
+
+ results[i] = [
+ SearchItem(
+ namespace=item.namespace,
+ key=item.key,
+ value=item.value,
+ created_at=item.created_at,
+ updated_at=item.updated_at,
+ score=float(score) if score is not None else None,
+ )
+ for score, item in kept
+ ]
+ else:
+ results[i] = [
+ SearchItem(
+ namespace=item.namespace,
+ key=item.key,
+ value=item.value,
+ created_at=item.created_at,
+ updated_at=item.updated_at,
+ )
+ for (item, _) in candidates[op.offset : op.offset + op.limit]
+ ]
+
+ def _prepare_ops(
+ self, ops: Iterable[Op]
+ ) -> tuple[
+ list[Result],
+ dict[tuple[tuple[str, ...], str], PutOp],
+ dict[int, tuple[SearchOp, list[tuple[Item, list[list[float]]]]]],
+ ]:
results: list[Result] = []
- for op in ops:
+ put_ops: dict[tuple[tuple[str, ...], str], PutOp] = {}
+ search_ops: dict[
+ int, tuple[SearchOp, list[tuple[Item, list[list[float]]]]]
+ ] = {}
+ for i, op in enumerate(ops):
if isinstance(op, GetOp):
item = self._data[op.namespace].get(op.key)
results.append(item)
elif isinstance(op, SearchOp):
- candidates = [
- item
- for namespace, items in self._data.items()
- if (
- namespace[: len(op.namespace_prefix)] == op.namespace_prefix
- if len(namespace) >= len(op.namespace_prefix)
- else False
- )
- for item in items.values()
- ]
- if op.filter:
- candidates = [
- item
- for item in candidates
- if item.value.items() >= op.filter.items()
- ]
- results.append(candidates[op.offset : op.offset + op.limit])
- elif isinstance(op, PutOp):
- if op.value is None:
- self._data[op.namespace].pop(op.key, None)
- elif op.key in self._data[op.namespace]:
- self._data[op.namespace][op.key].value = op.value
- self._data[op.namespace][op.key].updated_at = datetime.now(
- timezone.utc
- )
- else:
- self._data[op.namespace][op.key] = Item(
- value=op.value,
- key=op.key,
- namespace=op.namespace,
- created_at=datetime.now(timezone.utc),
- updated_at=datetime.now(timezone.utc),
- )
+ search_ops[i] = (op, self._filter_items(op))
results.append(None)
elif isinstance(op, ListNamespacesOp):
results.append(self._handle_list_namespaces(op))
- return results
+ elif isinstance(op, PutOp):
+ put_ops[(op.namespace, op.key)] = op
+ results.append(None)
+ else:
+ raise ValueError(f"Unknown operation type: {type(op)}")
- async def abatch(self, ops: Iterable[Op]) -> list[Result]:
- return self.batch(ops)
+ return results, put_ops, search_ops
+
+ def _apply_put_ops(self, put_ops: dict[tuple[tuple[str, ...], str], PutOp]) -> None:
+ for (namespace, key), op in put_ops.items():
+ if op.value is None:
+ self._data[namespace].pop(key, None)
+ self._vectors[namespace].pop(key, None)
+ else:
+ self._data[namespace][key] = Item(
+ value=op.value,
+ key=key,
+ namespace=namespace,
+ created_at=datetime.now(timezone.utc),
+ updated_at=datetime.now(timezone.utc),
+ )
+
+ def _extract_texts(
+ self, put_ops: dict[tuple[tuple[str, ...], str], PutOp]
+ ) -> dict[str, list[tuple[tuple[str, ...], str, str]]]:
+ if put_ops and self.index_config and self.embeddings:
+ to_embed = defaultdict(list)
+
+ for op in put_ops.values():
+ if op.value is not None and op.index is not False:
+ if op.index is None:
+ paths = self.index_config["__tokenized_fields"]
+ else:
+ paths = [(ix, tokenize_path(ix)) for ix in op.index]
+ for path, field in paths:
+ texts = get_text_at_path(op.value, field)
+ if texts:
+ if len(texts) > 1:
+ for i, text in enumerate(texts):
+ to_embed[text].append(
+ (op.namespace, op.key, f"{path}.{i}")
+ )
+
+ else:
+ to_embed[texts[0]].append((op.namespace, op.key, path))
+
+ return to_embed
+
+ return {}
+
+ def _insertinmem_store(
+ self,
+ to_embed: dict[str, list[tuple[tuple[str, ...], str, str]]],
+ embeddings: list[list[float]],
+ ) -> None:
+ indices = [index for indices in to_embed.values() for index in indices]
+ if len(indices) != len(embeddings):
+ raise ValueError(
+ f"Number of embeddings ({len(embeddings)}) does not"
+ f" match number of indices ({len(indices)})"
+ )
+ for embedding, (ns, key, path) in zip(embeddings, indices):
+ self._vectors[ns][key][path] = embedding
def _handle_list_namespaces(self, op: ListNamespacesOp) -> list[tuple[str, ...]]:
all_namespaces = list(
@@ -94,7 +394,52 @@ class InMemoryStore(BaseStore):
return namespaces[op.offset : op.offset + op.limit]
+@functools.lru_cache(maxsize=1)
+def _check_numpy() -> bool:
+ if bool(util.find_spec("numpy")):
+ return True
+ logger.warning(
+ "NumPy not found in the current Python environment. "
+ "The InMemoryStore will use a pure Python implementation for vector operations, "
+ "which may significantly impact performance, especially for large datasets or frequent searches. "
+ "For optimal speed and efficiency, consider installing NumPy: "
+ "pip install numpy"
+ )
+ return False
+
+
+def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
+ """
+ Compute cosine similarity between a vector X and a matrix Y.
+ Lazy import numpy for efficiency.
+ """
+ if _check_numpy():
+ import numpy as np # type: ignore
+
+ X_arr = np.array(X) if not isinstance(X, np.ndarray) else X
+ Y_arr = np.array(Y) if not isinstance(Y, np.ndarray) else Y
+ X_norm = np.linalg.norm(X_arr)
+ Y_norm = np.linalg.norm(Y_arr, axis=1)
+
+ # Avoid division by zero
+ mask = Y_norm != 0
+ similarities = np.zeros_like(Y_norm)
+ similarities[mask] = np.dot(Y_arr[mask], X_arr) / (Y_norm[mask] * X_norm)
+ return similarities.tolist()
+
+ similarities = []
+ for y in Y:
+ dot_product = sum(a * b for a, b in zip(X, y))
+ norm1 = sum(a * a for a in X) ** 0.5
+ norm2 = sum(a * a for a in y) ** 0.5
+ similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
+ similarities.append(similarity)
+
+ return similarities
+
+
def _does_match(match_condition: MatchCondition, key: tuple[str, ...]) -> bool:
+ """Whether a namespace key matches a match condition."""
match_type = match_condition.match_type
path = match_condition.path
@@ -117,3 +462,44 @@ def _does_match(match_condition: MatchCondition, key: tuple[str, ...]) -> bool:
return True
else:
raise ValueError(f"Unsupported match type: {match_type}")
+
+
+def _compare_values(item_value: Any, filter_value: Any) -> bool:
+ """Compare values in a JSONB-like way, handling nested objects."""
+ if isinstance(filter_value, dict):
+ if any(k.startswith("$") for k in filter_value):
+ return all(
+ _apply_operator(item_value, op_key, op_value)
+ for op_key, op_value in filter_value.items()
+ )
+ if not isinstance(item_value, dict):
+ return False
+ return all(
+ _compare_values(item_value.get(k), v) for k, v in filter_value.items()
+ )
+ elif isinstance(filter_value, (list, tuple)):
+ return (
+ isinstance(item_value, (list, tuple))
+ and len(item_value) == len(filter_value)
+ and all(_compare_values(iv, fv) for iv, fv in zip(item_value, filter_value))
+ )
+ else:
+ return item_value == filter_value
+
+
+def _apply_operator(value: Any, operator: str, op_value: Any) -> bool:
+ """Apply a comparison operator, matching PostgreSQL's JSONB behavior."""
+ if operator == "$eq":
+ return value == op_value
+ elif operator == "$gt":
+ return float(value) > float(op_value)
+ elif operator == "$gte":
+ return float(value) >= float(op_value)
+ elif operator == "$lt":
+ return float(value) < float(op_value)
+ elif operator == "$lte":
+ return float(value) <= float(op_value)
+ elif operator == "$ne":
+ return value != op_value
+ else:
+ raise ValueError(f"Unsupported operator: {operator}")
diff --git a/libs/checkpoint/pyproject.toml b/libs/checkpoint/pyproject.toml
index 278594fcb..deb7de5c4 100644
--- a/libs/checkpoint/pyproject.toml
+++ b/libs/checkpoint/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
-version = "2.0.6"
+version = "2.0.5"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
diff --git a/libs/checkpoint/tests/embed_test_utils.py b/libs/checkpoint/tests/embed_test_utils.py
new file mode 100644
index 000000000..d28cd959f
--- /dev/null
+++ b/libs/checkpoint/tests/embed_test_utils.py
@@ -0,0 +1,55 @@
+"""Embedding utilities for testing."""
+
+import math
+import random
+from collections import Counter, defaultdict
+from typing import Any
+
+from langchain_core.embeddings import Embeddings
+
+
+class CharacterEmbeddings(Embeddings):
+ """Simple character-frequency based embeddings using random projections."""
+
+ def __init__(self, dims: int = 50, seed: int = 42):
+ """Initialize with embedding dimensions and random seed."""
+ self._rng = random.Random(seed)
+ self.dims = dims
+ # Create projection vector for each character lazily
+ self._char_projections: defaultdict[str, list[float]] = defaultdict(
+ lambda: [
+ self._rng.gauss(0, 1 / math.sqrt(self.dims)) for _ in range(self.dims)
+ ]
+ )
+
+ def _embed_one(self, text: str) -> list[float]:
+ """Embed a single text."""
+ counts = Counter(text)
+ total = sum(counts.values())
+
+ if total == 0:
+ return [0.0] * self.dims
+
+ embedding = [0.0] * self.dims
+ for char, count in counts.items():
+ weight = count / total
+ char_proj = self._char_projections[char]
+ for i, proj in enumerate(char_proj):
+ embedding[i] += weight * proj
+
+ norm = math.sqrt(sum(x * x for x in embedding))
+ if norm > 0:
+ embedding = [x / norm for x in embedding]
+
+ return embedding
+
+ def embed_documents(self, texts: list[str]) -> list[list[float]]:
+ """Embed a list of documents."""
+ return [self._embed_one(text) for text in texts]
+
+ def embed_query(self, text: str) -> list[float]:
+ """Embed a query string."""
+ return self._embed_one(text)
+
+ def __eq__(self, other: Any) -> bool:
+ return isinstance(other, CharacterEmbeddings) and self.dims == other.dims
diff --git a/libs/checkpoint/tests/test_store.py b/libs/checkpoint/tests/test_store.py
index 0ecd4bd84..053d9a981 100644
--- a/libs/checkpoint/tests/test_store.py
+++ b/libs/checkpoint/tests/test_store.py
@@ -1,19 +1,30 @@
+# mypy: disable-error-code="operator"
import asyncio
+import json
from datetime import datetime
-from typing import Iterable
+from typing import Any, Iterable
import pytest
from pytest_mock import MockerFixture
-from langgraph.store.base import GetOp, InvalidNamespaceError, Item, Op, PutOp, Result
+from langgraph.store.base import (
+ GetOp,
+ InvalidNamespaceError,
+ Item,
+ Op,
+ PutOp,
+ Result,
+ get_text_at_path,
+)
from langgraph.store.base.batch import AsyncBatchedBaseStore
from langgraph.store.memory import InMemoryStore
+from tests.embed_test_utils import CharacterEmbeddings
class MockAsyncBatchedStore(AsyncBatchedBaseStore):
- def __init__(self) -> None:
+ def __init__(self, **kwargs: Any) -> None:
super().__init__()
- self._store = InMemoryStore()
+ self._store = InMemoryStore(**kwargs)
def batch(self, ops: Iterable[Op]) -> list[Result]:
return self._store.batch(ops)
@@ -22,6 +33,74 @@ class MockAsyncBatchedStore(AsyncBatchedBaseStore):
return self._store.batch(ops)
+def test_get_text_at_path() -> None:
+ nested_data = {
+ "name": "test",
+ "info": {
+ "age": 25,
+ "tags": ["a", "b", "c"],
+ "metadata": {"created": "2024-01-01", "updated": "2024-01-02"},
+ },
+ "items": [
+ {"id": 1, "value": "first", "tags": ["x", "y"]},
+ {"id": 2, "value": "second", "tags": ["y", "z"]},
+ {"id": 3, "value": "third", "tags": ["z", "w"]},
+ ],
+ "empty": None,
+ "zeros": [0, 0.0, "0"],
+ "empty_list": [],
+ "empty_dict": {},
+ }
+
+ assert get_text_at_path(nested_data, "$") == [
+ json.dumps(nested_data, sort_keys=True)
+ ]
+
+ assert get_text_at_path(nested_data, "name") == ["test"]
+ assert get_text_at_path(nested_data, "info.age") == ["25"]
+
+ assert get_text_at_path(nested_data, "info.metadata.created") == ["2024-01-01"]
+
+ assert get_text_at_path(nested_data, "items[0].value") == ["first"]
+ assert get_text_at_path(nested_data, "items[-1].value") == ["third"]
+ assert get_text_at_path(nested_data, "items[1].tags[0]") == ["y"]
+
+ values = get_text_at_path(nested_data, "items[*].value")
+ assert set(values) == {"first", "second", "third"}
+
+ metadata_dates = get_text_at_path(nested_data, "info.metadata.*")
+ assert set(metadata_dates) == {"2024-01-01", "2024-01-02"}
+ name_and_age = get_text_at_path(nested_data, "{name,info.age}")
+ assert set(name_and_age) == {"test", "25"}
+
+ item_fields = get_text_at_path(nested_data, "items[*].{id,value}")
+ assert set(item_fields) == {"1", "2", "3", "first", "second", "third"}
+
+ all_tags = get_text_at_path(nested_data, "items[*].tags[*]")
+ assert set(all_tags) == {"x", "y", "z", "w"}
+
+ assert get_text_at_path(None, "any.path") == []
+ assert get_text_at_path({}, "any.path") == []
+ assert get_text_at_path(nested_data, "") == [
+ json.dumps(nested_data, sort_keys=True)
+ ]
+ assert get_text_at_path(nested_data, "nonexistent") == []
+ assert get_text_at_path(nested_data, "items[99].value") == []
+ assert get_text_at_path(nested_data, "items[*].nonexistent") == []
+
+ assert get_text_at_path(nested_data, "empty") == []
+ assert get_text_at_path(nested_data, "empty_list") == ["[]"]
+ assert get_text_at_path(nested_data, "empty_dict") == ["{}"]
+
+ zeros = get_text_at_path(nested_data, "zeros[*]")
+ assert set(zeros) == {"0", "0.0"}
+
+ assert get_text_at_path(nested_data, "items[].value") == []
+ assert get_text_at_path(nested_data, "items[abc].value") == []
+ assert get_text_at_path(nested_data, "{unclosed") == []
+ assert get_text_at_path(nested_data, "nested[{invalid}]") == []
+
+
async def test_async_batch_store(mocker: MockerFixture) -> None:
abatch = mocker.stub()
@@ -304,12 +383,14 @@ async def test_cannot_put_empty_namespace() -> None:
await store.aput(("foo", "langgraph", "foo"), "bar", doc)
assert (await store.aget(("foo", "langgraph", "foo"), "bar")).value == doc # type: ignore[union-attr]
- assert (await store.asearch(("foo", "langgraph", "foo")))[0].value == doc
+ assert (await store.asearch(("foo", "langgraph", "foo"), query="bar"))[
+ 0
+ ].value == doc
await store.adelete(("foo", "langgraph", "foo"), "bar")
assert (await store.aget(("foo", "langgraph", "foo"), "bar")) is None
store.put(("foo", "langgraph", "foo"), "bar", doc)
assert store.get(("foo", "langgraph", "foo"), "bar").value == doc # type: ignore[union-attr]
- assert store.search(("foo", "langgraph", "foo"))[0].value == doc
+ assert store.search(("foo", "langgraph", "foo"), query="bar")[0].value == doc
store.delete(("foo", "langgraph", "foo"), "bar")
assert store.get(("foo", "langgraph", "foo"), "bar") is None
@@ -345,6 +426,9 @@ async def test_cannot_put_empty_namespace() -> None:
assert val is not None
assert val.value == doc
assert (await async_store.asearch(("foo", "langgraph", "foo")))[0].value == doc
+ assert (await async_store.asearch(("foo", "langgraph", "foo"), query="bar"))[
+ 0
+ ].value == doc
await async_store.adelete(("foo", "langgraph", "foo"), "bar")
assert (await async_store.aget(("foo", "langgraph", "foo"), "bar")) is None
@@ -420,3 +504,446 @@ async def test_async_batch_store_deduplication(mocker: MockerFixture) -> None:
assert results[0][0].value == doc2
abatch.reset_mock()
+
+
+@pytest.fixture
+def fake_embeddings() -> CharacterEmbeddings:
+ return CharacterEmbeddings(dims=500)
+
+
+def test_vector_store_initialization(fake_embeddings: CharacterEmbeddings) -> None:
+ """Test store initialization with embedding config."""
+ store = InMemoryStore(
+ index={"dims": fake_embeddings.dims, "embed": fake_embeddings}
+ )
+ assert store.index_config is not None
+ assert store.index_config["dims"] == fake_embeddings.dims
+ assert store.index_config["embed"] == fake_embeddings
+
+
+def test_vector_insert_with_auto_embedding(
+ fake_embeddings: CharacterEmbeddings,
+) -> None:
+ """Test inserting items that get auto-embedded."""
+ store = InMemoryStore(
+ index={"dims": fake_embeddings.dims, "embed": fake_embeddings}
+ )
+ docs = [
+ ("doc1", {"text": "short text"}),
+ ("doc2", {"text": "longer text document"}),
+ ("doc3", {"text": "longest text document here"}),
+ ("doc4", {"description": "text in description field"}),
+ ("doc5", {"content": "text in content field"}),
+ ("doc6", {"body": "text in body field"}),
+ ]
+
+ for key, value in docs:
+ store.put(("test",), key, value)
+
+ results = store.search(("test",), query="long text")
+ assert len(results) > 0
+
+ doc_order = [r.key for r in results]
+ assert "doc2" in doc_order
+ assert "doc3" in doc_order
+
+
+async def test_async_vector_insert_with_auto_embedding(
+ fake_embeddings: CharacterEmbeddings,
+) -> None:
+ """Test inserting items that get auto-embedded using async methods."""
+ store = InMemoryStore(
+ index={"dims": fake_embeddings.dims, "embed": fake_embeddings}
+ )
+ docs = [
+ ("doc1", {"text": "short text"}),
+ ("doc2", {"text": "longer text document"}),
+ ("doc3", {"text": "longest text document here"}),
+ ("doc4", {"description": "text in description field"}),
+ ("doc5", {"content": "text in content field"}),
+ ("doc6", {"body": "text in body field"}),
+ ]
+
+ for key, value in docs:
+ await store.aput(("test",), key, value)
+
+ results = await store.asearch(("test",), query="long text")
+ assert len(results) > 0
+
+ doc_order = [r.key for r in results]
+ assert "doc2" in doc_order
+ assert "doc3" in doc_order
+
+
+def test_vector_update_with_embedding(fake_embeddings: CharacterEmbeddings) -> None:
+ """Test that updating items properly updates their embeddings."""
+ store = InMemoryStore(
+ index={"dims": fake_embeddings.dims, "embed": fake_embeddings}
+ )
+ store.put(("test",), "doc1", {"text": "zany zebra Xerxes"})
+ store.put(("test",), "doc2", {"text": "something about dogs"})
+ store.put(("test",), "doc3", {"text": "text about birds"})
+
+ results_initial = store.search(("test",), query="Zany Xerxes")
+ assert len(results_initial) > 0
+ assert results_initial[0].key == "doc1"
+ initial_score = results_initial[0].score
+ assert initial_score is not None
+
+ store.put(("test",), "doc1", {"text": "new text about dogs"})
+
+ results_after = store.search(("test",), query="Zany Xerxes")
+ after_score = next((r.score for r in results_after if r.key == "doc1"), 0.0)
+ assert after_score is not None
+ assert after_score < initial_score
+
+ results_new = store.search(("test",), query="new text about dogs")
+ for r in results_new:
+ if r.key == "doc1":
+ assert r.score > after_score
+
+ # Don't index this one
+ store.put(("test",), "doc4", {"text": "new text about dogs"}, index=False)
+ results_new = store.search(("test",), query="new text about dogs", limit=3)
+ assert not any(r.key == "doc4" for r in results_new)
+
+
+async def test_async_vector_update_with_embedding(
+ fake_embeddings: CharacterEmbeddings,
+) -> None:
+ """Test that updating items properly updates their embeddings using async methods."""
+ store = InMemoryStore(
+ index={"dims": fake_embeddings.dims, "embed": fake_embeddings}
+ )
+ await store.aput(("test",), "doc1", {"text": "zany zebra Xerxes"})
+ await store.aput(("test",), "doc2", {"text": "something about dogs"})
+ await store.aput(("test",), "doc3", {"text": "text about birds"})
+
+ results_initial = await store.asearch(("test",), query="Zany Xerxes")
+ assert len(results_initial) > 0
+ assert results_initial[0].key == "doc1"
+ initial_score = results_initial[0].score
+
+ await store.aput(("test",), "doc1", {"text": "new text about dogs"})
+
+ results_after = await store.asearch(("test",), query="Zany Xerxes")
+ after_score = next((r.score for r in results_after if r.key == "doc1"), 0.0)
+ assert after_score is not None
+ assert after_score < initial_score
+
+ results_new = await store.asearch(("test",), query="new text about dogs")
+ for r in results_new:
+ if r.key == "doc1":
+ assert r.score is not None
+ assert r.score > after_score
+
+ # Don't index this one
+ await store.aput(("test",), "doc4", {"text": "new text about dogs"}, index=False)
+ results_new = await store.asearch(("test",), query="new text about dogs", limit=3)
+ assert not any(r.key == "doc4" for r in results_new)
+
+
+def test_vector_search_with_filters(fake_embeddings: CharacterEmbeddings) -> None:
+ """Test combining vector search with filters."""
+ inmem_store = InMemoryStore(
+ index={"dims": fake_embeddings.dims, "embed": fake_embeddings}
+ )
+ # Insert test documents
+ docs = [
+ ("doc1", {"text": "red apple", "color": "red", "score": 4.5}),
+ ("doc2", {"text": "red car", "color": "red", "score": 3.0}),
+ ("doc3", {"text": "green apple", "color": "green", "score": 4.0}),
+ ("doc4", {"text": "blue car", "color": "blue", "score": 3.5}),
+ ]
+
+ for key, value in docs:
+ inmem_store.put(("test",), key, value)
+
+ results = inmem_store.search(("test",), query="apple", filter={"color": "red"})
+ assert len(results) == 2
+ assert results[0].key == "doc1"
+
+ results = inmem_store.search(("test",), query="car", filter={"color": "red"})
+ assert len(results) == 2
+ assert results[0].key == "doc2"
+
+ results = inmem_store.search(
+ ("test",), query="bbbbluuu", filter={"score": {"$gt": 3.2}}
+ )
+ assert len(results) == 3
+ assert results[0].key == "doc4"
+
+ # Multiple filters
+ results = inmem_store.search(
+ ("test",), query="apple", filter={"score": {"$gte": 4.0}, "color": "green"}
+ )
+ assert len(results) == 1
+ assert results[0].key == "doc3"
+
+
+async def test_async_vector_search_with_filters(
+ fake_embeddings: CharacterEmbeddings,
+) -> None:
+ """Test combining vector search with filters using async methods."""
+ store = InMemoryStore(
+ index={"dims": fake_embeddings.dims, "embed": fake_embeddings}
+ )
+ # Insert test documents
+ docs = [
+ ("doc1", {"text": "red apple", "color": "red", "score": 4.5}),
+ ("doc2", {"text": "red car", "color": "red", "score": 3.0}),
+ ("doc3", {"text": "green apple", "color": "green", "score": 4.0}),
+ ("doc4", {"text": "blue car", "color": "blue", "score": 3.5}),
+ ]
+
+ for key, value in docs:
+ await store.aput(("test",), key, value)
+
+ results = await store.asearch(("test",), query="apple", filter={"color": "red"})
+ assert len(results) == 2
+ assert results[0].key == "doc1"
+
+ results = await store.asearch(("test",), query="car", filter={"color": "red"})
+ assert len(results) == 2
+ assert results[0].key == "doc2"
+
+ results = await store.asearch(
+ ("test",), query="bbbbluuu", filter={"score": {"$gt": 3.2}}
+ )
+ assert len(results) == 3
+ assert results[0].key == "doc4"
+
+ # Multiple filters
+ results = await store.asearch(
+ ("test",), query="apple", filter={"score": {"$gte": 4.0}, "color": "green"}
+ )
+ assert len(results) == 1
+ assert results[0].key == "doc3"
+
+
+async def test_async_batched_vector_search_concurrent(
+ fake_embeddings: CharacterEmbeddings,
+) -> None:
+ """Test concurrent vector search operations using async batched store."""
+ store = MockAsyncBatchedStore(
+ index={"dims": fake_embeddings.dims, "embed": fake_embeddings}
+ )
+
+ colors = ["red", "blue", "green", "yellow", "purple"]
+ items = ["apple", "car", "house", "book", "phone"]
+ scores = [3.0, 3.5, 4.0, 4.5, 5.0]
+
+ docs = []
+ for i in range(50):
+ color = colors[i % len(colors)]
+ item = items[i % len(items)]
+ score = scores[i % len(scores)]
+ docs.append(
+ (
+ f"doc{i}",
+ {"text": f"{color} {item}", "color": color, "score": score, "index": i},
+ )
+ )
+ coros = [
+ *[store.aput(("test",), key, value) for key, value in docs],
+ *[store.adelete(("test",), key) for key, value in docs],
+ *[store.aput(("test",), key, value) for key, value in docs],
+ ]
+ await asyncio.gather(*coros)
+
+ # Prepare multiple search queries with different filters
+ search_queries: list[tuple[str, dict[str, Any]]] = [
+ ("apple", {"color": "red"}),
+ ("car", {"color": "blue"}),
+ ("house", {"color": "green"}),
+ ("phone", {"score": {"$gt": 4.99}}),
+ ("book", {"score": {"$lte": 3.5}}),
+ ("apple", {"score": {"$gte": 3.0}, "color": "red"}),
+ ("car", {"score": {"$lt": 5.1}, "color": "blue"}),
+ ("house", {"index": {"$gt": 25}}),
+ ("phone", {"index": {"$lte": 10}}),
+ ]
+
+ all_results = await asyncio.gather(
+ *[
+ store.asearch(("test",), query=query, filter=filter_)
+ for query, filter_ in search_queries
+ ]
+ )
+
+ for results, (query, filter_) in zip(all_results, search_queries):
+ assert len(results) > 0, f"No results for query '{query}' with filter {filter_}"
+
+ for result in results:
+ if "color" in filter_:
+ assert result.value["color"] == filter_["color"]
+
+ if "score" in filter_:
+ score = result.value["score"]
+ for op, value in filter_["score"].items():
+ if op == "$gt":
+ assert score > value
+ elif op == "$gte":
+ assert score >= value
+ elif op == "$lt":
+ assert score < value
+ elif op == "$lte":
+ assert score <= value
+
+ if "index" in filter_:
+ index = result.value["index"]
+ for op, value in filter_["index"].items():
+ if op == "$gt":
+ assert index > value
+ elif op == "$gte":
+ assert index >= value
+ elif op == "$lt":
+ assert index < value
+ elif op == "$lte":
+ assert index <= value
+
+
+def test_vector_search_pagination(fake_embeddings: CharacterEmbeddings) -> None:
+ """Test pagination with vector search."""
+ store = InMemoryStore(
+ index={"dims": fake_embeddings.dims, "embed": fake_embeddings}
+ )
+ for i in range(5):
+ store.put(("test",), f"doc{i}", {"text": f"test document number {i}"})
+
+ results_page1 = store.search(("test",), query="test", limit=2)
+ results_page2 = store.search(("test",), query="test", limit=2, offset=2)
+
+ assert len(results_page1) == 2
+ assert len(results_page2) == 2
+ assert results_page1[0].key != results_page2[0].key
+
+ all_results = store.search(("test",), query="test", limit=10)
+ assert len(all_results) == 5
+
+
+async def test_async_vector_search_pagination(
+ fake_embeddings: CharacterEmbeddings,
+) -> None:
+ """Test pagination with vector search using async methods."""
+ store = InMemoryStore(
+ index={"dims": fake_embeddings.dims, "embed": fake_embeddings}
+ )
+ for i in range(5):
+ await store.aput(("test",), f"doc{i}", {"text": f"test document number {i}"})
+
+ results_page1 = await store.asearch(("test",), query="test", limit=2)
+ results_page2 = await store.asearch(("test",), query="test", limit=2, offset=2)
+
+ assert len(results_page1) == 2
+ assert len(results_page2) == 2
+ assert results_page1[0].key != results_page2[0].key
+
+ all_results = await store.asearch(("test",), query="test", limit=10)
+ assert len(all_results) == 5
+
+
+async def test_embed_with_path(fake_embeddings: CharacterEmbeddings) -> None:
+ # Test store-level field configuration
+ store = InMemoryStore(
+ index={
+ "dims": fake_embeddings.dims,
+ "embed": fake_embeddings,
+ # Key 2 isn't included. Don't index it.
+ "fields": ["key0", "key1", "key3"],
+ }
+ )
+ # This will have 2 vectors representing it
+ doc1 = {
+ # Omit key0 - check it doesn't raise an error
+ "key1": "xxx",
+ "key2": "yyy",
+ "key3": "zzz",
+ }
+ # This will have 3 vectors representing it
+ doc2 = {
+ "key0": "uuu",
+ "key1": "vvv",
+ "key2": "www",
+ "key3": "xxx",
+ }
+ await store.aput(("test",), "doc1", doc1)
+ await store.aput(("test",), "doc2", doc2)
+
+ # doc2.key3 and doc1.key1 both would have the highest score
+ results = await store.asearch(("test",), query="xxx")
+ assert len(results) == 2
+ assert results[0].key != results[1].key
+ ascore = results[0].score
+ bscore = results[1].score
+ assert ascore == bscore
+ assert ascore is not None and bscore is not None
+
+ results = await store.asearch(("test",), query="uuu")
+ assert len(results) == 2
+ assert results[0].key != results[1].key
+ assert results[0].key == "doc2"
+ assert results[0].score is not None and results[0].score > results[1].score
+ assert ascore == pytest.approx(results[0].score, abs=1e-5)
+
+ # Un-indexed - will have low results for both. Not zero (because we're projecting)
+ # but less than the above.
+ results = await store.asearch(("test",), query="www")
+ assert len(results) == 2
+ assert results[0].score < ascore
+ assert results[1].score < ascore
+
+ # Test operation-level field configuration
+ store_no_defaults = InMemoryStore(
+ index={
+ "dims": fake_embeddings.dims,
+ "embed": fake_embeddings,
+ "fields": ["key17"],
+ }
+ )
+
+ doc3 = {
+ "key0": "aaa",
+ "key1": "bbb",
+ "key2": "ccc",
+ "key3": "ddd",
+ }
+ doc4 = {
+ "key0": "eee",
+ "key1": "bbb", # Same as doc3.key1
+ "key2": "fff",
+ "key3": "ggg",
+ }
+
+ await store_no_defaults.aput(("test",), "doc3", doc3, index=["key0", "key1"])
+ await store_no_defaults.aput(("test",), "doc4", doc4, index=["key1", "key3"])
+
+ results = await store_no_defaults.asearch(("test",), query="aaa")
+ assert len(results) == 2
+ assert results[0].key == "doc3"
+ assert results[0].score is not None and results[0].score > results[1].score
+
+ results = await store_no_defaults.asearch(("test",), query="ggg")
+ assert len(results) == 2
+ assert results[0].key == "doc4"
+ assert results[0].score is not None and results[0].score > results[1].score
+
+ results = await store_no_defaults.asearch(("test",), query="bbb")
+ assert len(results) == 2
+ assert results[0].key != results[1].key
+ assert results[0].score == results[1].score
+
+ results = await store_no_defaults.asearch(("test",), query="ccc")
+ assert len(results) == 2
+ assert all(r.score < ascore for r in results)
+
+ doc5 = {
+ "key0": "hhh",
+ "key1": "iii",
+ }
+ await store_no_defaults.aput(("test",), "doc5", doc5, index=False)
+
+ results = await store_no_defaults.asearch(("test",), query="hhh")
+ assert len(results) == 3
+ doc5_result = next(r for r in results if r.key == "doc5")
+ assert doc5_result.score is None
From 769f6a1925c4ceca30a49627cd68009ca95e3e7e Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 27 Nov 2024 15:59:48 -0800
Subject: [PATCH 065/149] Fix
---
libs/sdk-py/langgraph_sdk/sse.py | 2 +-
libs/sdk-py/pyproject.toml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/libs/sdk-py/langgraph_sdk/sse.py b/libs/sdk-py/langgraph_sdk/sse.py
index b87788387..8b019e4a6 100644
--- a/libs/sdk-py/langgraph_sdk/sse.py
+++ b/libs/sdk-py/langgraph_sdk/sse.py
@@ -42,7 +42,7 @@ class BytesLineDecoder:
if len(lines) == 1 and not trailing_newline:
# No new lines, buffer the input and continue.
- self.buffer.append(lines[0])
+ self.buffer.write(lines[0])
return []
if self.buffer:
diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml
index c9e0401da..735954803 100644
--- a/libs/sdk-py/pyproject.toml
+++ b/libs/sdk-py/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
-version = "0.1.37"
+version = "0.1.38"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"
From 53ec7c41b2bd4261ba3790f417724a420132d863 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 27 Nov 2024 17:31:21 -0800
Subject: [PATCH 066/149] Revert "sdk-py: Fix SSE parsing to split lines only
\n \r \r\n per SSE spec"
This reverts commit dc09b134007c2a8c054b4db6ff09cf779366ebd0.
---
libs/sdk-py/langgraph_sdk/client.py | 5 +-
libs/sdk-py/langgraph_sdk/sse.py | 106 ----------------------------
2 files changed, 2 insertions(+), 109 deletions(-)
delete mode 100644 libs/sdk-py/langgraph_sdk/sse.py
diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py
index a018bb3b6..6a3bb6c9c 100644
--- a/libs/sdk-py/langgraph_sdk/client.py
+++ b/libs/sdk-py/langgraph_sdk/client.py
@@ -59,7 +59,6 @@ from langgraph_sdk.schema import (
ThreadStatus,
ThreadUpdateStateResponse,
)
-from langgraph_sdk.sse import EventSource
logger = logging.getLogger(__name__)
@@ -293,7 +292,7 @@ class HttpClient:
else:
logger.error(f"Error from langgraph-api: {body}", exc_info=e)
raise e
- async for event in EventSource(sse.response).aiter_sse():
+ async for event in sse.aiter_sse():
yield StreamPart(
event.event, orjson.loads(event.data) if event.data else None
)
@@ -2427,7 +2426,7 @@ class SyncHttpClient:
else:
logger.error(f"Error from langgraph-api: {body}", exc_info=e)
raise e
- for event in EventSource(sse.response).iter_sse():
+ for event in sse.iter_sse():
yield StreamPart(
event.event, orjson.loads(event.data) if event.data else None
)
diff --git a/libs/sdk-py/langgraph_sdk/sse.py b/libs/sdk-py/langgraph_sdk/sse.py
deleted file mode 100644
index 8b019e4a6..000000000
--- a/libs/sdk-py/langgraph_sdk/sse.py
+++ /dev/null
@@ -1,106 +0,0 @@
-"""Adapted from httpx_sse to split lines on \n, \r, \r\n per the SSE spec."""
-
-import io
-from typing import AsyncIterator, Iterator
-
-import httpx
-import httpx_sse
-import httpx_sse._decoders
-
-
-class BytesLineDecoder:
- """
- Handles incrementally reading lines from text.
-
- Has the same behaviour as the stdllib bytes splitlines,
- but handling the input iteratively.
- """
-
- def __init__(self) -> None:
- self.buffer = io.BytesIO()
- self.trailing_cr: bool = False
-
- def decode(self, text: bytes) -> list[bytes]:
- # See https://docs.python.org/3/glossary.html#term-universal-newlines
- NEWLINE_CHARS = b"\n\r"
-
- # We always push a trailing `\r` into the next decode iteration.
- if self.trailing_cr:
- text = b"\r" + text
- self.trailing_cr = False
- if text.endswith(b"\r"):
- self.trailing_cr = True
- text = text[:-1]
-
- if not text:
- # NOTE: the edge case input of empty text doesn't occur in practice,
- # because other httpx internals filter out this value
- return [] # pragma: no cover
-
- trailing_newline = text[-1] in NEWLINE_CHARS
- lines = text.splitlines()
-
- if len(lines) == 1 and not trailing_newline:
- # No new lines, buffer the input and continue.
- self.buffer.write(lines[0])
- return []
-
- if self.buffer:
- # Include any existing buffer in the first portion of the
- # splitlines result.
- lines = [self.buffer.getvalue() + lines[0]] + lines[1:]
- self.buffer.truncate(0)
-
- if not trailing_newline:
- # If the last segment of splitlines is not newline terminated,
- # then drop it from our output and start a new buffer.
- self.buffer.write(lines.pop())
-
- return lines
-
- def flush(self) -> list[bytes]:
- if not self.buffer and not self.trailing_cr:
- return []
-
- lines = [self.buffer.getvalue()] if self.buffer else []
- self.buffer.truncate(0)
- self.trailing_cr = False
- return lines
-
-
-async def aiter_lines_raw(response: httpx.Response) -> AsyncIterator[bytes]:
- decoder = BytesLineDecoder()
- async for chunk in response.aiter_bytes():
- for line in decoder.decode(chunk):
- yield line
- for line in decoder.flush():
- yield line
-
-
-def iter_lines_raw(response: httpx.Response) -> Iterator[bytes]:
- decoder = BytesLineDecoder()
- for chunk in response.iter_bytes():
- for line in decoder.decode(chunk):
- yield line
- for line in decoder.flush():
- yield line
-
-
-class EventSource(httpx_sse.EventSource):
- async def aiter_sse(self) -> AsyncIterator[httpx_sse.ServerSentEvent]:
- self._check_content_type()
- decoder = httpx_sse._decoders.SSEDecoder()
- async for line in aiter_lines_raw(self._response):
- line = line.rstrip(b"\n")
- sse = decoder.decode(line.decode())
- if sse is not None:
- yield sse
-
- def iter_sse(self) -> Iterator[httpx_sse.ServerSentEvent]:
- self._check_content_type()
- decoder = httpx_sse._decoders.SSEDecoder()
- for line in iter_lines_raw(self._response):
- line = line.rstrip(b"\n")
- sse = decoder.decode(line.decode())
- if sse is not None:
- yield sse
From 4576a259dd92850de369be041c66173622dfce92 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 27 Nov 2024 17:32:07 -0800
Subject: [PATCH 067/149] sdk-py 0.1.39
---
libs/sdk-py/pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml
index 735954803..7c8af51b2 100644
--- a/libs/sdk-py/pyproject.toml
+++ b/libs/sdk-py/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
-version = "0.1.38"
+version = "0.1.39"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"
From d767af421b5ff6ebbc6853b1c85454b97db0de64 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 27 Nov 2024 20:40:12 -0800
Subject: [PATCH 068/149] feat: Add vector search (#2535)
- Initializing the store with an 'embedding config' -> this contains the
'dims' (used to create the table) and the encoder object (rn langchain
embeddings object, though that is ......)
- Call setup() -> creates the vector table.
Each document has 1 or more vectors associated with it for each json
path in the embedding config.
Would welcome critique and requests!
Leaving the params as the defaults for pgvector but open to feedback if
you think it's important to be able to more transparently configure that
in setup()
```python
from typing import TypedDict, List, Dict, Any, Optional
from langchain_openai import OpenAIEmbeddings
from langgraph.graph import StateGraph
from langgraph.store.postgres import PostgresStore
emb_config = {
"dims": 1536, # OpenAI embedding dimensions
"embed": OpenAIEmbeddings(model="text-embedding-3-small"),
"distance_type": "cosine",
}
with PostgresStore.from_conn_string(
"postgres://postgres:postgres@localhost:5441",
embedding=emb_config,
) as store:
store.setup()
# Define the state type for our graph
class State(TypedDict):
query: str
results: Optional[List[Dict[str, Any]]]
def put_stuff(state: State) -> State:
docs = [
("doc1", {"text": "red apple in kitchen"}),
("doc2", {"text": "blue car in garage"}),
("doc3", {"text": "green apple on table"}),
]
for key, value in docs:
store.put(("docs",), key, value)
def search_stuff(state: State) -> State:
"""Search for documents using vector similarity."""
results = store.search(("docs",), query=state["query"])
return {"results": results}
builder = StateGraph(State)
builder.add_node(put_stuff)
builder.add_node(search_stuff)
builder.add_edge("__start__", "put_stuff")
builder.add_edge("put_stuff", "search_stuff")
# Compile
with PostgresStore.from_conn_string(
"postgres://postgres:postgres@localhost:5441",
embedding=emb_config,
) as store:
chain = builder.compile(store=store)
result = chain.invoke({"query": "sour apple"})
# Print results
for doc in result["results"]:
print(doc.key)
print(doc.value)
print(doc.response_metadata)
```
---
libs/checkpoint-postgres/Makefile | 6 +-
.../langgraph/checkpoint/postgres/__init__.py | 3 +-
.../checkpoint/postgres/_ainternal.py | 3 +-
.../checkpoint/postgres/_internal.py | 3 +-
.../langgraph/checkpoint/postgres/aio.py | 5 +-
.../langgraph/checkpoint/postgres/base.py | 5 +-
.../langgraph/store/postgres/aio.py | 322 ++++++-----
.../langgraph/store/postgres/base.py | 513 +++++++++++++++--
libs/checkpoint-postgres/pyproject.toml | 2 +-
libs/checkpoint-postgres/tests/__init__.py | 0
.../tests/compose-postgres.yml | 3 +-
libs/checkpoint-postgres/tests/conftest.py | 12 +-
.../tests/embed_test_utils.py | 55 ++
libs/checkpoint-postgres/tests/test_async.py | 2 +-
.../tests/test_async_store.py | 515 ++++++++++--------
libs/checkpoint-postgres/tests/test_store.py | 357 +++++++++++-
libs/checkpoint-postgres/tests/test_sync.py | 2 +-
libs/checkpoint/pyproject.toml | 2 +-
18 files changed, 1382 insertions(+), 428 deletions(-)
create mode 100644 libs/checkpoint-postgres/tests/__init__.py
create mode 100644 libs/checkpoint-postgres/tests/embed_test_utils.py
diff --git a/libs/checkpoint-postgres/Makefile b/libs/checkpoint-postgres/Makefile
index 33ed2a3c4..adf92f262 100644
--- a/libs/checkpoint-postgres/Makefile
+++ b/libs/checkpoint-postgres/Makefile
@@ -5,7 +5,11 @@
######################
start-postgres:
- POSTGRES_VERSION=${POSTGRES_VERSION:-16} docker compose -f tests/compose-postgres.yml up -V --force-recreate --wait
+ POSTGRES_VERSION=${POSTGRES_VERSION:-16} docker compose -f tests/compose-postgres.yml up -V --force-recreate --wait || ( \
+ echo "Failed to start PostgreSQL, printing logs..."; \
+ docker compose -f tests/compose-postgres.yml logs; \
+ exit 1 \
+ )
stop-postgres:
docker compose -f tests/compose-postgres.yml down
diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py
index 1a3aff119..d8af3aeca 100644
--- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py
+++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py
@@ -1,6 +1,7 @@
import threading
+from collections.abc import Iterator, Sequence
from contextlib import contextmanager
-from typing import Any, Iterator, Optional, Sequence
+from typing import Any, Optional
from langchain_core.runnables import RunnableConfig
from psycopg import Capabilities, Connection, Cursor, Pipeline
diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_ainternal.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_ainternal.py
index a0b8b10f5..33d299029 100644
--- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_ainternal.py
+++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_ainternal.py
@@ -1,7 +1,8 @@
"""Shared async utility functions for the Postgres checkpoint & storage classes."""
+from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
-from typing import AsyncIterator, Union
+from typing import Union
from psycopg import AsyncConnection
from psycopg.rows import DictRow
diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_internal.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_internal.py
index b703262f2..5d2926084 100644
--- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_internal.py
+++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_internal.py
@@ -1,7 +1,8 @@
"""Shared utility functions for the Postgres checkpoint & storage classes."""
+from collections.abc import Iterator
from contextlib import contextmanager
-from typing import Iterator, Union
+from typing import Union
from psycopg import Connection
from psycopg.rows import DictRow
diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
index 5b07e0067..440cb452e 100644
--- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
+++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
@@ -1,6 +1,7 @@
import asyncio
+from collections.abc import AsyncIterator, Iterator, Sequence
from contextlib import asynccontextmanager
-from typing import Any, AsyncIterator, Iterator, Optional, Sequence
+from typing import Any, Optional
from langchain_core.runnables import RunnableConfig
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
@@ -385,7 +386,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
while True:
try:
yield asyncio.run_coroutine_threadsafe(
- anext(aiter_),
+ anext(aiter_), # noqa: F821
self.loop,
).result()
except StopAsyncIteration:
diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py
index ae65cab68..90ba81686 100644
--- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py
+++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py
@@ -1,5 +1,6 @@
import random
-from typing import Any, List, Optional, Sequence, Tuple, cast
+from collections.abc import Sequence
+from typing import Any, Optional, cast
from langchain_core.runnables import RunnableConfig
from psycopg.types.json import Jsonb
@@ -249,7 +250,7 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
config: Optional[RunnableConfig],
filter: MetadataInput,
before: Optional[RunnableConfig] = None,
- ) -> Tuple[str, List[Any]]:
+ ) -> tuple[str, list[Any]]:
"""Return WHERE clause predicates for alist() given config, filter, before.
This method returns a tuple of a string and a tuple of values. The string
diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/aio.py b/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
index b90a9a0d5..282f08186 100644
--- a/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
+++ b/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
@@ -1,16 +1,8 @@
import asyncio
import logging
+from collections.abc import AsyncIterator, Iterable, Sequence
from contextlib import asynccontextmanager
-from typing import (
- Any,
- AsyncIterator,
- Callable,
- Iterable,
- Optional,
- Sequence,
- Union,
- cast,
-)
+from typing import Any, Callable, Optional, Union, cast
import orjson
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
@@ -19,13 +11,23 @@ from psycopg.rows import DictRow, dict_row
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.postgres import _ainternal
-from langgraph.store.base import GetOp, ListNamespacesOp, Op, PutOp, Result, SearchOp
+from langgraph.store.base import (
+ GetOp,
+ ListNamespacesOp,
+ Op,
+ PutOp,
+ Result,
+ SearchOp,
+)
from langgraph.store.base.batch import AsyncBatchedBaseStore
from langgraph.store.postgres.base import (
+ _PLACEHOLDER,
BasePostgresStore,
PoolConfig,
+ PostgresIndexConfig,
Row,
_decode_ns_bytes,
+ _ensure_index_config,
_group_ops,
_row_to_item,
_row_to_search_item,
@@ -35,7 +37,14 @@ logger = logging.getLogger(__name__)
class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Conn]):
- __slots__ = ("_deserializer", "pipe", "lock", "supports_pipeline")
+ __slots__ = (
+ "_deserializer",
+ "pipe",
+ "lock",
+ "supports_pipeline",
+ "index_config",
+ "embeddings",
+ )
def __init__(
self,
@@ -45,6 +54,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
deserializer: Optional[
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
] = None,
+ index: Optional[PostgresIndexConfig] = None,
) -> None:
if isinstance(conn, AsyncConnectionPool) and pipe is not None:
raise ValueError(
@@ -57,6 +67,12 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
self.lock = asyncio.Lock()
self.loop = asyncio.get_running_loop()
self.supports_pipeline = Capabilities().has_pipeline()
+ self.index_config = index
+ if self.index_config:
+ self.embeddings, self.index_config = _ensure_index_config(self.index_config)
+
+ else:
+ self.embeddings = None
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
grouped_ops, num_ops = _group_ops(ops)
@@ -71,13 +87,117 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
return results
+ def batch(self, ops: Iterable[Op]) -> list[Result]:
+ return asyncio.run_coroutine_threadsafe(self.abatch(ops), self.loop).result()
+
+ @classmethod
+ @asynccontextmanager
+ async def from_conn_string(
+ cls,
+ conn_string: str,
+ *,
+ pipeline: bool = False,
+ pool_config: Optional[PoolConfig] = None,
+ index: Optional[PostgresIndexConfig] = None,
+ ) -> AsyncIterator["AsyncPostgresStore"]:
+ """Create a new AsyncPostgresStore instance from a connection string.
+
+ Args:
+ conn_string (str): The Postgres connection info string.
+ pipeline (bool): Whether to use AsyncPipeline (only for single connections)
+ pool_config (Optional[PoolConfig]): Configuration for the connection pool.
+ If provided, will create a connection pool and use it instead of a single connection.
+ This overrides the `pipeline` argument.
+ index (Optional[PostgresIndexConfig]): The embedding config.
+
+ Returns:
+ AsyncPostgresStore: A new AsyncPostgresStore instance.
+ """
+ if pool_config is not None:
+ pc = pool_config.copy()
+ async with cast(
+ AsyncConnectionPool[AsyncConnection[DictRow]],
+ AsyncConnectionPool(
+ conn_string,
+ min_size=pc.pop("min_size", 1),
+ max_size=pc.pop("max_size", None),
+ kwargs={
+ "autocommit": True,
+ "prepare_threshold": 0,
+ "row_factory": dict_row,
+ **(pc.pop("kwargs", None) or {}),
+ },
+ **cast(dict, pc),
+ ),
+ ) as pool:
+ yield cls(conn=pool, index=index)
+ else:
+ async with await AsyncConnection.connect(
+ conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
+ ) as conn:
+ if pipeline:
+ async with conn.pipeline() as pipe:
+ yield cls(conn=conn, pipe=pipe, index=index)
+ else:
+ yield cls(conn=conn, index=index)
+
+ async def setup(self) -> None:
+ """Set up the store database asynchronously.
+
+ 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 the store is used.
+ """
+
+ async def _get_version(cur: AsyncCursor[DictRow], table: str) -> int:
+ try:
+ await cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
+ row = await cur.fetchone()
+ if row is None:
+ version = -1
+ else:
+ version = row["v"]
+ except UndefinedTable:
+ version = -1
+ await cur.execute(
+ f"""
+ CREATE TABLE IF NOT EXISTS {table} (
+ v INTEGER PRIMARY KEY
+ )
+ """
+ )
+ return version
+
+ async with self._cursor() as cur:
+ version = await _get_version(cur, table="store_migrations")
+ for v, sql in enumerate(self.MIGRATIONS[version + 1 :], start=version + 1):
+ await cur.execute(sql)
+ await cur.execute("INSERT INTO store_migrations (v) VALUES (%s)", (v,))
+
+ if self.index_config:
+ version = await _get_version(cur, table="vector_migrations")
+ for v, migration in enumerate(
+ self.VECTOR_MIGRATIONS[version + 1 :], start=version + 1
+ ):
+ sql = migration.sql
+ if migration.params:
+ params = {
+ k: v(self) if v is not None and callable(v) else v
+ for k, v in migration.params.items()
+ }
+ sql = sql % params
+ await cur.execute(sql)
+ await cur.execute(
+ "INSERT INTO vector_migrations (v) VALUES (%s)", (v,)
+ )
+
async def _execute_batch(
self,
grouped_ops: dict,
results: list[Result],
conn: AsyncConnection[DictRow],
) -> None:
- async with self._cursor(conn, pipeline=True) as cur:
+ async with self._cursor(pipeline=True) as cur:
if GetOp in grouped_ops:
await self._batch_get_ops(
cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]),
@@ -132,7 +252,31 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
put_ops: Sequence[tuple[int, PutOp]],
cur: AsyncCursor[DictRow],
) -> None:
- queries = self._get_batch_PUT_queries(put_ops)
+ queries, embedding_request = self._prepare_batch_PUT_queries(put_ops)
+ if embedding_request:
+ if self.embeddings is None:
+ # Should not get here since the embedding config is required
+ # to return an embedding_request above
+ raise ValueError(
+ "Embedding configuration is required for vector operations "
+ f"(for semantic search). "
+ f"Please provide an EmbeddingConfig when initializing the {self.__class__.__name__}."
+ )
+ query, txt_params = embedding_request
+ vectors = await self.embeddings.aembed_documents(
+ [param[-1] for param in txt_params]
+ )
+ queries.append(
+ (
+ query,
+ [
+ p
+ for (ns, k, pathname, _), vector in zip(txt_params, vectors)
+ for p in (ns, k, pathname, vector)
+ ],
+ )
+ )
+
for query, params in queries:
await cur.execute(query, params)
@@ -142,8 +286,19 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
results: list[Result],
cur: AsyncCursor[DictRow],
) -> None:
- queries = self._get_batch_search_queries(search_ops)
- for (query, params), (idx, _) in zip(queries, search_ops):
+ queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
+
+ if embedding_requests and self.embeddings:
+ vectors = await self.embeddings.aembed_documents(
+ [query for _, query in embedding_requests]
+ )
+ for (idx, _), vector in zip(embedding_requests, vectors):
+ _paramslist = queries[idx][1]
+ for i in range(len(_paramslist)):
+ if _paramslist[i] is _PLACEHOLDER:
+ _paramslist[i] = vector
+
+ for (idx, _), (query, params) in zip(search_ops, queries):
await cur.execute(query, params)
rows = cast(list[Row], await cur.fetchall())
items = [
@@ -169,129 +324,46 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
@asynccontextmanager
async def _cursor(
- self, conn: AsyncConnection[DictRow], *, pipeline: bool = False
- ) -> AsyncIterator[AsyncCursor[Any]]:
+ self, *, pipeline: bool = False
+ ) -> AsyncIterator[AsyncCursor[DictRow]]:
"""Create a database cursor as a context manager.
Args:
- conn: The database connection to use
pipeline: whether to use pipeline for the DB operations inside the context manager.
Will be applied regardless of whether the PostgresStore instance was initialized with a pipeline.
If pipeline mode is not supported, will fall back to using transaction context manager.
"""
- if self.pipe:
- # a connection in pipeline mode can be used concurrently
- # in multiple threads/coroutines, but only one cursor can be
- # used at a time
- async with conn.cursor(binary=True) as cur:
+ async with _ainternal.get_connection(self.conn) as conn:
+ if self.pipe:
+ # a connection in pipeline mode can be used concurrently
+ # in multiple threads/coroutines, but only one cursor can be
+ # used at a time
try:
- yield cur
+ async with conn.cursor(binary=True, row_factory=dict_row) as cur:
+ yield cur
finally:
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
- if self.supports_pipeline:
- async with self.lock, conn.pipeline(), conn.cursor(binary=True) as cur:
- yield cur
+ elif pipeline:
+ # a connection not in pipeline mode can only be used by one
+ # thread/coroutine at a time, so we acquire a lock
+ if self.supports_pipeline:
+ async with (
+ self.lock,
+ conn.pipeline(),
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
+ ):
+ yield cur
+ else:
+ async with (
+ self.lock,
+ conn.transaction(),
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
+ ):
+ yield cur
else:
async with (
self.lock,
- conn.transaction(),
conn.cursor(binary=True) as cur,
):
yield cur
- else:
- async with conn.cursor(binary=True) as cur:
- yield cur
-
- def batch(self, ops: Iterable[Op]) -> list[Result]:
- return asyncio.run_coroutine_threadsafe(self.abatch(ops), self.loop).result()
-
- @classmethod
- @asynccontextmanager
- async def from_conn_string(
- cls,
- conn_string: str,
- *,
- pipeline: bool = False,
- pool_config: Optional[PoolConfig] = None,
- ) -> AsyncIterator["AsyncPostgresStore"]:
- """Create a new AsyncPostgresStore instance from a connection string.
-
- Args:
- conn_string (str): The Postgres connection info string.
- pipeline (bool): Whether to use AsyncPipeline (only for single connections)
- pool_config (Optional[PoolConfig]): Configuration for the connection pool.
- If provided, will create a connection pool and use it instead of a single connection.
- This overrides the `pipeline` argument.
-
- Returns:
- AsyncPostgresStore: A new AsyncPostgresStore instance.
- """
- if pool_config is not None:
- pc = pool_config.copy()
- async with cast(
- AsyncConnectionPool[AsyncConnection[DictRow]],
- AsyncConnectionPool(
- conn_string,
- min_size=pc.pop("min_size", 1),
- max_size=pc.pop("max_size", None),
- kwargs={
- "autocommit": True,
- "prepare_threshold": 0,
- "row_factory": dict_row,
- **(pc.pop("kwargs", None) or {}),
- },
- **cast(dict, pc),
- ),
- ) as pool:
- yield cls(conn=pool)
- else:
- async with await AsyncConnection.connect(
- conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
- ) as conn:
- if pipeline:
- async with conn.pipeline() as pipe:
- yield cls(conn=conn, pipe=pipe)
- else:
- yield cls(conn=conn)
-
- async def setup(self) -> None:
- """Set up the store database asynchronously.
-
- 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 the store is used.
- """
- async with _ainternal.get_connection(self.conn) as conn:
- async with conn.cursor() as cur:
- try:
- await cur.execute(
- "SELECT v FROM store_migrations ORDER BY v DESC LIMIT 1"
- )
- row = cast(dict, await cur.fetchone())
- if row is None:
- version = -1
- else:
- version = row["v"]
- except UndefinedTable:
- version = -1
- # Create store_migrations table if it doesn't exist
- await cur.execute(
- """
- CREATE TABLE IF NOT EXISTS store_migrations (
- v INTEGER PRIMARY KEY
- )
- """
- )
- for v, migration in enumerate(
- self.MIGRATIONS[version + 1 :], start=version + 1
- ):
- await cur.execute(migration)
- await cur.execute(
- "INSERT INTO store_migrations (v) VALUES (%s)", (v,)
- )
- if self.pipe:
- await self.pipe.sync()
diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/base.py b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
index 200218c0d..2a908c90e 100644
--- a/libs/checkpoint-postgres/langgraph/store/postgres/base.py
+++ b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
@@ -3,16 +3,17 @@ import json
import logging
import threading
from collections import defaultdict
+from collections.abc import Iterable, Iterator, Sequence
from contextlib import contextmanager
from datetime import datetime
from typing import (
+ TYPE_CHECKING,
Any,
Callable,
Generic,
- Iterable,
- Iterator,
+ Literal,
+ NamedTuple,
Optional,
- Sequence,
TypeVar,
Union,
cast,
@@ -31,6 +32,7 @@ from langgraph.checkpoint.postgres import _internal as _pg_internal
from langgraph.store.base import (
BaseStore,
GetOp,
+ IndexConfig,
Item,
ListNamespacesOp,
Op,
@@ -38,12 +40,25 @@ from langgraph.store.base import (
Result,
SearchItem,
SearchOp,
+ ensure_embeddings,
+ get_text_at_path,
+ tokenize_path,
)
+if TYPE_CHECKING:
+ from langchain_core.embeddings import Embeddings
+
logger = logging.getLogger(__name__)
-MIGRATIONS = [
+class Migration(NamedTuple):
+ """A database migration with optional conditions and parameters."""
+
+ sql: str
+ params: Optional[dict[str, Any]] = None
+
+
+MIGRATIONS: Sequence[str] = [
"""
CREATE TABLE IF NOT EXISTS store (
-- 'prefix' represents the doc's 'namespace'
@@ -61,6 +76,39 @@ CREATE INDEX IF NOT EXISTS store_prefix_idx ON store USING btree (prefix text_pa
""",
]
+VECTOR_MIGRATIONS: Sequence[Migration] = [
+ Migration(
+ """
+CREATE EXTENSION IF NOT EXISTS vector;
+""",
+ ),
+ Migration(
+ """
+CREATE TABLE IF NOT EXISTS store_vectors (
+ prefix text NOT NULL,
+ key text NOT NULL,
+ field_name text NOT NULL,
+ embedding %(vector_type)s(%(dims)s),
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (prefix, key, field_name),
+ FOREIGN KEY (prefix, key) REFERENCES store(prefix, key) ON DELETE CASCADE
+);
+""",
+ params={
+ "dims": lambda store: store.index_config["dims"],
+ "vector_type": lambda store: (
+ cast(PostgresIndexConfig, store.index_config)
+ .get("ann_index_config", {})
+ .get("vector_type", "vector")
+ ),
+ },
+ ),
+ # TODO: Add an HNSW or IVFFlat index depending on config
+ # First must improve the search query when filtering by
+ # namespace
+]
+
C = TypeVar("C", bound=Union[_pg_internal.Conn, _ainternal.Conn])
@@ -89,10 +137,39 @@ class PoolConfig(TypedDict, total=False):
"""
+class ANNIndexConfig(TypedDict, total=False):
+ """Configuration for vector index in PostgreSQL store."""
+
+ vector_type: Literal["vector", "halfvec"]
+ """Type of vector storage to use.
+ Options:
+ - 'vector': Regular vectors (default)
+ - 'halfvec': Half-precision vectors for reduced memory usage
+ """
+
+
+class PostgresIndexConfig(IndexConfig, total=False):
+ """Configuration for vector embeddings in PostgreSQL store with pgvector-specific options.
+
+ Extends EmbeddingConfig with additional configuration for pgvector index and vector types.
+ """
+
+ ann_index_config: ANNIndexConfig
+ """Specific configuration for the chosen index type (HNSW or IVF Flat)."""
+ distance_type: Literal["l2", "inner_product", "cosine"]
+ """Distance metric to use for vector similarity search:
+ - 'l2': Euclidean distance
+ - 'inner_product': Dot product
+ - 'cosine': Cosine similarity
+ """
+
+
class BasePostgresStore(Generic[C]):
MIGRATIONS = MIGRATIONS
+ VECTOR_MIGRATIONS = VECTOR_MIGRATIONS
conn: C
_deserializer: Optional[Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]]
+ index_config: Optional[PostgresIndexConfig]
def _get_batch_GET_ops_queries(
self,
@@ -114,10 +191,13 @@ class BasePostgresStore(Generic[C]):
results.append((query, params, namespace, items))
return results
- def _get_batch_PUT_queries(
+ def _prepare_batch_PUT_queries(
self,
put_ops: Sequence[tuple[int, PutOp]],
- ) -> list[tuple[str, Sequence]]:
+ ) -> tuple[
+ list[tuple[str, Sequence]],
+ Optional[tuple[str, Sequence[tuple[str, str, str, str]]]],
+ ]:
# Last-write wins
dedupped_ops: dict[tuple[tuple[str, ...], str], PutOp] = {}
for _, op in put_ops:
@@ -144,60 +224,182 @@ class BasePostgresStore(Generic[C]):
)
params = (_namespace_to_text(namespace), *keys)
queries.append((query, params))
+ embedding_request: Optional[tuple[str, Sequence[tuple[str, str, str, str]]]] = (
+ None
+ )
if inserts:
values = []
insertion_params = []
+ vector_values = []
+ embedding_request_params = []
+
+ # First handle main store insertions
for op in inserts:
values.append("(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)")
insertion_params.extend(
[
_namespace_to_text(op.namespace),
op.key,
- Jsonb(op.value),
+ Jsonb(cast(dict, op.value)),
]
)
+
+ # Then handle embeddings if configured
+ if self.index_config:
+ for op in inserts:
+ if op.index is False:
+ continue
+ value = op.value
+ ns = _namespace_to_text(op.namespace)
+ k = op.key
+
+ if op.index is None:
+ paths = self.index_config["__tokenized_fields"]
+ else:
+ paths = [(ix, tokenize_path(ix)) for ix in op.index]
+
+ for path, tokenized_path in paths:
+ texts = get_text_at_path(value, tokenized_path)
+ for i, text in enumerate(texts):
+ pathname = f"{path}.{i}" if len(texts) > 1 else path
+ vector_values.append(
+ "(%s, %s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
+ )
+ embedding_request_params.append((ns, k, pathname, text))
+
values_str = ",".join(values)
query = f"""
INSERT INTO store (prefix, key, value, created_at, updated_at)
VALUES {values_str}
ON CONFLICT (prefix, key) DO UPDATE
- SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP
+ SET value = EXCLUDED.value,
+ updated_at = CURRENT_TIMESTAMP
"""
queries.append((query, insertion_params))
- return queries
+ if vector_values:
+ values_str = ",".join(vector_values)
+ query = f"""
+ INSERT INTO store_vectors (prefix, key, field_name, embedding, created_at, updated_at)
+ VALUES {values_str}
+ ON CONFLICT (prefix, key, field_name) DO UPDATE
+ SET embedding = EXCLUDED.embedding,
+ updated_at = CURRENT_TIMESTAMP
+ """
+ embedding_request = (query, embedding_request_params)
- def _get_batch_search_queries(
+ return queries, embedding_request
+
+ def _prepare_batch_search_queries(
self,
search_ops: Sequence[tuple[int, SearchOp]],
- ) -> list[tuple[str, Sequence]]:
- queries: list[tuple[str, Sequence]] = []
- for _, op in search_ops:
- query = """
- SELECT prefix, key, value, created_at, updated_at
- FROM store
- WHERE prefix LIKE %s
- """
- params: list = [f"{_namespace_to_text(op.namespace_prefix)}%"]
+ ) -> tuple[
+ list[tuple[str, list[Union[None, str, list[float]]]]], # queries, params
+ list[tuple[int, str]], # idx, query_text pairs to embed
+ ]:
+ queries = []
+ embedding_requests = []
+ for idx, (_, op) in enumerate(search_ops):
+ # Build filter conditions first
+ filter_params = []
+ filter_conditions = []
if op.filter:
- filter_conditions = []
for key, value in op.filter.items():
- if isinstance(value, list):
- filter_conditions.append("value->%s @> %s::jsonb")
- params.extend([key, json.dumps(value)])
+ if isinstance(value, dict):
+ for op_name, val in value.items():
+ condition, filter_params_ = self._get_filter_condition(
+ key, op_name, val
+ )
+ filter_conditions.append(condition)
+ filter_params.extend(filter_params_)
else:
filter_conditions.append("value->%s = %s::jsonb")
- params.extend([key, json.dumps(value)])
- query += " AND " + " AND ".join(filter_conditions)
+ filter_params.extend([key, json.dumps(value)])
- # Note: we will need to not do this if sim/keyword search
- # is used
- query += " ORDER BY updated_at DESC LIMIT %s OFFSET %s"
- params.extend([op.limit, op.offset])
+ # Vector search branch
+ if op.query and self.index_config:
+ embedding_requests.append((idx, op.query))
- queries.append((query, params))
- return queries
+ score_operator = _get_distance_operator(self)
+ vector_type = (
+ cast(PostgresIndexConfig, self.index_config)
+ .get("ann_index_config", {})
+ .get("vector_type", "vector")
+ )
+
+ if (
+ vector_type == "bit"
+ and self.index_config.get("distance_type") == "hamming"
+ ):
+ score_operator = score_operator % (
+ "%s",
+ self.index_config["dims"],
+ )
+ else:
+ score_operator = score_operator % (
+ "%s",
+ vector_type,
+ )
+
+ vectors_per_doc_estimate = self.index_config["__estimated_num_vectors"]
+ expanded_limit = (op.limit * vectors_per_doc_estimate * 2) + 1
+
+ # Vector search with CTE for proper score handling
+ filter_str = (
+ ""
+ if not filter_conditions
+ else " AND " + " AND ".join(filter_conditions)
+ )
+ base_query = f"""
+ WITH scored AS (
+ SELECT s.prefix, s.key, s.value, s.created_at, s.updated_at, {score_operator} AS score
+ FROM store s
+ JOIN store_vectors sv ON s.prefix = sv.prefix AND s.key = sv.key
+ WHERE s.prefix LIKE %s {filter_str}
+ ORDER BY {score_operator} DESC
+ LIMIT %s
+ )
+ SELECT * FROM (
+ SELECT DISTINCT ON (prefix, key)
+ prefix, key, value, created_at, updated_at, score
+ FROM scored
+ ORDER BY prefix, key, score DESC
+ ) AS unique_docs
+ ORDER BY score DESC
+ LIMIT %s
+ OFFSET %s
+ """
+ params = [
+ _PLACEHOLDER, # Vector placeholder
+ f"{_namespace_to_text(op.namespace_prefix)}%",
+ *filter_params,
+ _PLACEHOLDER,
+ expanded_limit,
+ op.limit,
+ op.offset,
+ ]
+
+ # Regular search branch
+ else:
+ base_query = """
+ SELECT prefix, key, value, created_at, updated_at
+ FROM store
+ WHERE prefix LIKE %s
+ """
+ params = [f"{_namespace_to_text(op.namespace_prefix)}%"]
+
+ if filter_conditions:
+ params.extend(filter_params)
+ base_query += " AND " + " AND ".join(filter_conditions)
+
+ base_query += " ORDER BY updated_at DESC"
+ base_query += " LIMIT %s OFFSET %s"
+ params.extend([op.limit, op.offset])
+
+ queries.append((base_query, params))
+
+ return queries, embedding_requests
def _get_batch_list_namespaces_queries(
self,
@@ -249,13 +451,37 @@ class BasePostgresStore(Generic[C]):
query += " ORDER BY truncated_prefix LIMIT %s OFFSET %s"
params.extend([op.limit, op.offset])
- queries.append((query, params))
+ queries.append((query, tuple(params)))
return queries
+ def _get_filter_condition(self, key: str, op: str, value: Any) -> tuple[str, list]:
+ """Helper to generate filter conditions."""
+ if op == "$eq":
+ return "value->%s = %s::jsonb", [key, json.dumps(value)]
+ elif op == "$gt":
+ return "value->>%s > %s", [key, str(value)]
+ elif op == "$gte":
+ return "value->>%s >= %s", [key, str(value)]
+ elif op == "$lt":
+ return "value->>%s < %s", [key, str(value)]
+ elif op == "$lte":
+ return "value->>%s <= %s", [key, str(value)]
+ elif op == "$ne":
+ return "value->%s != %s::jsonb", [key, json.dumps(value)]
+ else:
+ raise ValueError(f"Unsupported operator: {op}")
+
class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
- __slots__ = ("_deserializer", "pipe", "lock", "supports_pipeline")
+ __slots__ = (
+ "_deserializer",
+ "pipe",
+ "lock",
+ "supports_pipeline",
+ "index_config",
+ "embeddings",
+ )
def __init__(
self,
@@ -265,6 +491,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
deserializer: Optional[
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
] = None,
+ index: Optional[PostgresIndexConfig] = None,
) -> None:
super().__init__()
self._deserializer = deserializer
@@ -272,6 +499,11 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
self.pipe = pipe
self.supports_pipeline = Capabilities().has_pipeline()
self.lock = threading.Lock()
+ self.index_config = index
+ if self.index_config:
+ self.embeddings, self.index_config = _ensure_index_config(self.index_config)
+ else:
+ self.embeddings = None
@classmethod
@contextmanager
@@ -281,15 +513,18 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
*,
pipeline: bool = False,
pool_config: Optional[PoolConfig] = None,
+ index: Optional[PostgresIndexConfig] = None,
) -> Iterator["PostgresStore"]:
"""Create a new PostgresStore instance from a connection string.
Args:
conn_string (str): The Postgres connection info string.
- pipeline (bool): whether to use Pipeline (only for single connections)
+ pipeline (bool): whether to use Pipeline
pool_config (Optional[PoolArgs]): Configuration for the connection pool.
If provided, will create a connection pool and use it instead of a single connection.
This overrides the `pipeline` argument.
+ index (Optional[PostgresIndexConfig]): The index configuration for the store.
+
Returns:
PostgresStore: A new PostgresStore instance.
"""
@@ -310,16 +545,16 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
**cast(dict, pc),
),
) as pool:
- yield cls(conn=pool)
+ yield cls(conn=pool, index=index)
else:
with Connection.connect(
conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
) as conn:
if pipeline:
with conn.pipeline() as pipe:
- yield cls(conn, pipe=pipe)
+ yield cls(conn, pipe=pipe, index=index)
else:
- yield cls(conn)
+ yield cls(conn, index=index)
@contextmanager
def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor[DictRow]]:
@@ -419,7 +654,32 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
put_ops: Sequence[tuple[int, PutOp]],
cur: Cursor[DictRow],
) -> None:
- queries = self._get_batch_PUT_queries(put_ops)
+ queries, embedding_request = self._prepare_batch_PUT_queries(put_ops)
+ if embedding_request:
+ if self.embeddings is None:
+ # Should not get here since the embedding config is required
+ # to return an embedding_request above
+ raise ValueError(
+ "Embedding configuration is required for vector operations "
+ f"(for semantic search). "
+ f"Please provide an Embeddings when initializing the {self.__class__.__name__}."
+ )
+ query, txt_params = embedding_request
+ # Update the params to replace the raw text with the vectors
+ vectors = self.embeddings.embed_documents(
+ [param[-1] for param in txt_params]
+ )
+ queries.append(
+ (
+ query,
+ [
+ p
+ for (ns, k, pathname, _), vector in zip(txt_params, vectors)
+ for p in (ns, k, pathname, vector)
+ ],
+ )
+ )
+
for query, params in queries:
cur.execute(query, params)
@@ -429,9 +689,20 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
results: list[Result],
cur: Cursor[DictRow],
) -> None:
- for (query, params), (idx, _) in zip(
- self._get_batch_search_queries(search_ops), search_ops
- ):
+ queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
+
+ if embedding_requests and self.embeddings:
+ embeddings = self.embeddings.embed_documents(
+ [query for _, query in embedding_requests]
+ )
+ for (idx, _), embedding in zip(embedding_requests, embeddings):
+ _paramslist = queries[idx][1]
+ for i in range(len(_paramslist)):
+ if _paramslist[i] is _PLACEHOLDER:
+ _paramslist[i] = embedding
+
+ for (idx, _), (query, params) in zip(search_ops, queries):
+ # Execute the actual query
cur.execute(query, params)
rows = cast(list[Row], cur.fetchall())
results[idx] = [
@@ -463,9 +734,10 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
already exist and runs database migrations. It MUST be called directly by the user
the first time the store is used.
"""
- with self._cursor() as cur:
+
+ def _get_version(cur: Cursor[dict[str, Any]], table: str) -> int:
try:
- cur.execute("SELECT v FROM store_migrations ORDER BY v DESC LIMIT 1")
+ cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
row = cast(dict, cur.fetchone())
if row is None:
version = -1
@@ -474,18 +746,35 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
except UndefinedTable:
version = -1
cur.execute(
- """
- CREATE TABLE IF NOT EXISTS store_migrations (
+ f"""
+ CREATE TABLE IF NOT EXISTS {table} (
v INTEGER PRIMARY KEY
)
"""
)
- for v, migration in enumerate(
- self.MIGRATIONS[version + 1 :], start=version + 1
- ):
- cur.execute(migration)
+ return version
+
+ with self._cursor() as cur:
+ version = _get_version(cur, table="store_migrations")
+ for v, sql in enumerate(self.MIGRATIONS[version + 1 :], start=version + 1):
+ cur.execute(sql)
cur.execute("INSERT INTO store_migrations (v) VALUES (%s)", (v,))
+ if self.index_config:
+ version = _get_version(cur, table="vector_migrations")
+ for v, migration in enumerate(
+ self.VECTOR_MIGRATIONS[version + 1 :], start=version + 1
+ ):
+ sql = migration.sql
+ if migration.params:
+ params = {
+ k: v(self) if v is not None and callable(v) else v
+ for k, v in migration.params.items()
+ }
+ sql = sql % params
+ cur.execute(sql)
+ cur.execute("INSERT INTO vector_migrations (v) VALUES (%s)", (v,))
+
class Row(TypedDict):
key: str
@@ -495,6 +784,45 @@ class Row(TypedDict):
updated_at: datetime
+# Private utilities
+
+_DEFAULT_ANN_CONFIG = ANNIndexConfig(
+ vector_type="vector",
+)
+
+
+def _get_vector_type_ops(store: BasePostgresStore) -> str:
+ """Get the vector type operator class based on config."""
+ if not store.index_config:
+ return "vector_cosine_ops"
+
+ config = cast(PostgresIndexConfig, store.index_config)
+ index_config = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
+ vector_type = cast(str, index_config.get("vector_type", "vector"))
+ if vector_type not in ("vector", "halfvec"):
+ raise ValueError(
+ f"Vector type must be 'vector' or 'halfvec', got {vector_type}"
+ )
+
+ distance_type = config.get("distance_type", "cosine")
+
+ # For regular vectors
+ type_prefix = {"vector": "vector", "halfvec": "halfvec"}[vector_type]
+
+ if distance_type not in ("l2", "inner_product", "cosine"):
+ raise ValueError(
+ f"Vector type {vector_type} only supports 'l2', 'inner_product', or 'cosine' distance, got {distance_type}"
+ )
+
+ distance_suffix = {
+ "l2": "l2_ops",
+ "inner_product": "ip_ops",
+ "cosine": "cosine_ops",
+ }[distance_type]
+
+ return f"{type_prefix}_{distance_suffix}"
+
+
def _namespace_to_text(
namespace: tuple[str, ...], handle_wildcards: bool = False
) -> str:
@@ -510,16 +838,26 @@ def _row_to_item(
*,
loader: Optional[Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]] = None,
) -> Item:
- """Convert a row from the database into an Item."""
- loader = loader or _json_loads
+ """Convert a row from the database into an Item.
+
+ Args:
+ namespace: Item namespace
+ row: Database row
+ loader: Optional value loader for non-dict values
+ """
val = row["value"]
- return Item(
- value=val if isinstance(val, dict) else loader(val),
- key=row["key"],
- namespace=namespace,
- created_at=row["created_at"],
- updated_at=row["updated_at"],
- )
+ if not isinstance(val, dict):
+ val = (loader or _json_loads)(val)
+
+ kwargs = {
+ "key": row["key"],
+ "namespace": namespace,
+ "value": val,
+ "created_at": row["created_at"],
+ "updated_at": row["updated_at"],
+ }
+
+ return Item(**kwargs)
def _row_to_search_item(
@@ -575,3 +913,62 @@ def _decode_ns_bytes(namespace: Union[str, bytes, list]) -> tuple[str, ...]:
if isinstance(namespace, bytes):
namespace = namespace.decode()[1:]
return tuple(namespace.split("."))
+
+
+def _get_distance_operator(store: Any) -> str:
+ """Get the distance operator and score expression based on config."""
+ # Note: Today, we are not using ANN indices due to restrictions
+ # on PGVector's support for mixing vector and non-vector filters
+ # To use the index, PGVector expects:
+ # - ORDER BY the operator NOT an expression (even negation blocks it)
+ # - ASCENDING order
+ # - Any WHERE clause should be over a partial index.
+ # If we violate any of these, it will use a sequential scan
+ # See https://github.com/pgvector/pgvector/issues/216 and the
+ # pgvector documentation for more details.
+ if not store.index_config:
+ raise ValueError(
+ "Embedding configuration is required for vector operations "
+ f"(for semantic search). "
+ f"Please provide an Embeddings when initializing the {store.__class__.__name__}."
+ )
+
+ config = cast(PostgresIndexConfig, store.index_config)
+ distance_type = config.get("distance_type", "cosine")
+
+ if distance_type == "l2":
+ return "1 - (sv.embedding <-> %s::%s)"
+ elif distance_type == "inner_product":
+ return "-(sv.embedding <#> %s::%s)"
+ else: # cosine
+ return "1 - (sv.embedding <=> %s::%s)"
+
+
+def _ensure_index_config(
+ index_config: PostgresIndexConfig,
+) -> tuple[Optional["Embeddings"], PostgresIndexConfig]:
+ index_config = index_config.copy()
+ tokenized: list[tuple[str, Union[Literal["$"], list[str]]]] = []
+ tot = 0
+ text_fields = index_config.get("text_fields") or ["$"]
+ if isinstance(text_fields, str):
+ text_fields = [text_fields]
+ if not isinstance(text_fields, list):
+ raise ValueError(f"Text fields must be a list or a string. Got {text_fields}")
+ for p in text_fields:
+ if p == "$":
+ tokenized.append((p, "$"))
+ tot += 1
+ else:
+ toks = tokenize_path(p)
+ tokenized.append((p, toks))
+ tot += len(toks)
+ index_config["__tokenized_fields"] = tokenized
+ index_config["__estimated_num_vectors"] = tot
+ embeddings = ensure_embeddings(
+ index_config.get("embed"),
+ )
+ return embeddings, index_config
+
+
+_PLACEHOLDER = object()
diff --git a/libs/checkpoint-postgres/pyproject.toml b/libs/checkpoint-postgres/pyproject.toml
index b879a3a6e..bfefeba1d 100644
--- a/libs/checkpoint-postgres/pyproject.toml
+++ b/libs/checkpoint-postgres/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-postgres"
-version = "2.0.4"
+version = "2.0.5"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
diff --git a/libs/checkpoint-postgres/tests/__init__.py b/libs/checkpoint-postgres/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/libs/checkpoint-postgres/tests/compose-postgres.yml b/libs/checkpoint-postgres/tests/compose-postgres.yml
index a8a6c1e74..721784433 100644
--- a/libs/checkpoint-postgres/tests/compose-postgres.yml
+++ b/libs/checkpoint-postgres/tests/compose-postgres.yml
@@ -1,12 +1,13 @@
services:
postgres-test:
- image: postgres:${POSTGRES_VERSION:-16}
+ image: pgvector/pgvector:pg${POSTGRES_VERSION:-16}
ports:
- "5441:5432"
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
+ command: ["postgres", "-c", "shared_preload_libraries=vector"]
healthcheck:
test: pg_isready -U postgres
start_period: 10s
diff --git a/libs/checkpoint-postgres/tests/conftest.py b/libs/checkpoint-postgres/tests/conftest.py
index 56d199812..ab59dbc6b 100644
--- a/libs/checkpoint-postgres/tests/conftest.py
+++ b/libs/checkpoint-postgres/tests/conftest.py
@@ -1,10 +1,12 @@
-from typing import AsyncIterator
+from collections.abc import AsyncIterator
import pytest
from psycopg import AsyncConnection
from psycopg.errors import UndefinedTable
from psycopg.rows import DictRow, dict_row
+from tests.embed_test_utils import CharacterEmbeddings
+
DEFAULT_URI = "postgres://postgres:postgres@localhost:5441/postgres?sslmode=disable"
@@ -31,3 +33,11 @@ async def clear_test_db(conn: AsyncConnection[DictRow]) -> None:
await conn.execute("DELETE FROM store")
except UndefinedTable:
pass
+
+
+@pytest.fixture
+def fake_embeddings() -> CharacterEmbeddings:
+ return CharacterEmbeddings(dims=500)
+
+
+VECTOR_TYPES = ["vector", "halfvec"]
diff --git a/libs/checkpoint-postgres/tests/embed_test_utils.py b/libs/checkpoint-postgres/tests/embed_test_utils.py
new file mode 100644
index 000000000..d28cd959f
--- /dev/null
+++ b/libs/checkpoint-postgres/tests/embed_test_utils.py
@@ -0,0 +1,55 @@
+"""Embedding utilities for testing."""
+
+import math
+import random
+from collections import Counter, defaultdict
+from typing import Any
+
+from langchain_core.embeddings import Embeddings
+
+
+class CharacterEmbeddings(Embeddings):
+ """Simple character-frequency based embeddings using random projections."""
+
+ def __init__(self, dims: int = 50, seed: int = 42):
+ """Initialize with embedding dimensions and random seed."""
+ self._rng = random.Random(seed)
+ self.dims = dims
+ # Create projection vector for each character lazily
+ self._char_projections: defaultdict[str, list[float]] = defaultdict(
+ lambda: [
+ self._rng.gauss(0, 1 / math.sqrt(self.dims)) for _ in range(self.dims)
+ ]
+ )
+
+ def _embed_one(self, text: str) -> list[float]:
+ """Embed a single text."""
+ counts = Counter(text)
+ total = sum(counts.values())
+
+ if total == 0:
+ return [0.0] * self.dims
+
+ embedding = [0.0] * self.dims
+ for char, count in counts.items():
+ weight = count / total
+ char_proj = self._char_projections[char]
+ for i, proj in enumerate(char_proj):
+ embedding[i] += weight * proj
+
+ norm = math.sqrt(sum(x * x for x in embedding))
+ if norm > 0:
+ embedding = [x / norm for x in embedding]
+
+ return embedding
+
+ def embed_documents(self, texts: list[str]) -> list[list[float]]:
+ """Embed a list of documents."""
+ return [self._embed_one(text) for text in texts]
+
+ def embed_query(self, text: str) -> list[float]:
+ """Embed a query string."""
+ return self._embed_one(text)
+
+ def __eq__(self, other: Any) -> bool:
+ return isinstance(other, CharacterEmbeddings) and self.dims == other.dims
diff --git a/libs/checkpoint-postgres/tests/test_async.py b/libs/checkpoint-postgres/tests/test_async.py
index 256bbe8a3..73c376fd2 100644
--- a/libs/checkpoint-postgres/tests/test_async.py
+++ b/libs/checkpoint-postgres/tests/test_async.py
@@ -1,7 +1,6 @@
from typing import Any
import pytest
-from conftest import DEFAULT_URI # type: ignore
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
@@ -11,6 +10,7 @@ from langgraph.checkpoint.base import (
empty_checkpoint,
)
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
+from tests.conftest import DEFAULT_URI
class TestAsyncPostgresSaver:
diff --git a/libs/checkpoint-postgres/tests/test_async_store.py b/libs/checkpoint-postgres/tests/test_async_store.py
index 71aaa4e36..eda0e2820 100644
--- a/libs/checkpoint-postgres/tests/test_async_store.py
+++ b/libs/checkpoint-postgres/tests/test_async_store.py
@@ -1,14 +1,22 @@
# type: ignore
+import itertools
import sys
import uuid
-from typing import AsyncIterator
+from collections.abc import AsyncIterator
+from contextlib import asynccontextmanager
+from typing import Any, Optional
import pytest
-from conftest import DEFAULT_URI # type: ignore
+from langchain_core.embeddings import Embeddings
from psycopg import AsyncConnection
from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
from langgraph.store.postgres import AsyncPostgresStore
+from tests.conftest import (
+ DEFAULT_URI,
+ VECTOR_TYPES,
+ CharacterEmbeddings,
+)
@pytest.fixture(scope="function", params=["default", "pipe", "pool"])
@@ -181,272 +189,319 @@ async def test_batch_list_namespaces_ops(store: AsyncPostgresStore) -> None:
assert ("test", "namespace2") in results[0]
-class TestAsyncPostgresStore:
- @pytest.fixture(autouse=True)
- async def setup(self) -> None:
- async with AsyncPostgresStore.from_conn_string(DEFAULT_URI) as store:
+@asynccontextmanager
+async def _create_vector_store(
+ vector_type: str,
+ distance_type: str,
+ fake_embeddings: CharacterEmbeddings,
+ text_fields: Optional[list[str]] = None,
+) -> AsyncIterator[AsyncPostgresStore]:
+ """Create a store with vector search enabled."""
+ if sys.version_info < (3, 10):
+ pytest.skip("Async Postgres tests require Python 3.10+")
+
+ database = f"test_{uuid.uuid4().hex[:16]}"
+ uri_parts = DEFAULT_URI.split("/")
+ uri_base = "/".join(uri_parts[:-1])
+ query_params = ""
+ if "?" in uri_parts[-1]:
+ db_name, query_params = uri_parts[-1].split("?", 1)
+ query_params = "?" + query_params
+
+ conn_string = f"{uri_base}/{database}{query_params}"
+ admin_conn_string = DEFAULT_URI
+
+ index_config = {
+ "dims": fake_embeddings.dims,
+ "embed": fake_embeddings,
+ "ann_index_config": {
+ "vector_type": vector_type,
+ },
+ "distance_type": distance_type,
+ "text_fields": text_fields,
+ }
+
+ async with await AsyncConnection.connect(
+ admin_conn_string, autocommit=True
+ ) as conn:
+ await conn.execute(f"CREATE DATABASE {database}")
+ try:
+ async with AsyncPostgresStore.from_conn_string(
+ conn_string,
+ index=index_config,
+ ) as store:
await store.setup()
+ yield store
+ finally:
+ async with await AsyncConnection.connect(
+ admin_conn_string, autocommit=True
+ ) as conn:
+ await conn.execute(f"DROP DATABASE {database}")
- async def test_basic_store_ops(self) -> None:
- async with AsyncPostgresStore.from_conn_string(DEFAULT_URI) as store:
- namespace = ("test", "documents")
- item_id = "doc1"
- item_value = {"title": "Test Document", "content": "Hello, World!"}
- await store.aput(namespace, item_id, item_value)
- item = await store.aget(namespace, item_id)
+@pytest.fixture(
+ scope="function",
+ params=[
+ (vector_type, distance_type)
+ for vector_type in VECTOR_TYPES
+ for distance_type in (
+ ["hamming"] if vector_type == "bit" else ["l2", "inner_product", "cosine"]
+ )
+ ],
+ ids=lambda p: f"{p[0]}_{p[1]}",
+)
+async def vector_store(
+ request,
+ fake_embeddings: CharacterEmbeddings,
+) -> AsyncIterator[AsyncPostgresStore]:
+ """Create a store with vector search enabled."""
+ vector_type, distance_type = request.param
+ async with _create_vector_store(
+ vector_type, distance_type, fake_embeddings
+ ) as store:
+ yield store
- assert item
- assert item.namespace == namespace
- assert item.key == item_id
- assert item.value == item_value
- updated_value = {
- "title": "Updated Test Document",
- "content": "Hello, LangGraph!",
- }
- await store.aput(namespace, item_id, updated_value)
- updated_item = await store.aget(namespace, item_id)
+async def test_vector_store_initialization(
+ vector_store: AsyncPostgresStore, fake_embeddings: CharacterEmbeddings
+) -> None:
+ """Test store initialization with embedding config."""
+ assert vector_store.index_config is not None
+ assert vector_store.index_config["dims"] == fake_embeddings.dims
+ if isinstance(vector_store.index_config["embed"], Embeddings):
+ assert vector_store.index_config["embed"] == fake_embeddings
- assert updated_item.value == updated_value
- assert updated_item.updated_at > item.updated_at
- different_namespace = ("test", "other_documents")
- item_in_different_namespace = await store.aget(different_namespace, item_id)
- assert item_in_different_namespace is None
- new_item_id = "doc2"
- new_item_value = {"title": "Another Document", "content": "Greetings!"}
- await store.aput(namespace, new_item_id, new_item_value)
+async def test_vector_insert_with_auto_embedding(
+ vector_store: AsyncPostgresStore,
+) -> None:
+ """Test inserting items that get auto-embedded."""
+ docs = [
+ ("doc1", {"text": "short text"}),
+ ("doc2", {"text": "longer text document"}),
+ ("doc3", {"text": "longest text document here"}),
+ ("doc4", {"description": "text in description field"}),
+ ("doc5", {"content": "text in content field"}),
+ ("doc6", {"body": "text in body field"}),
+ ]
- search_results = await store.asearch(["test"], limit=10)
- items = search_results
- assert len(items) == 2
- assert any(item.key == item_id for item in items)
- assert any(item.key == new_item_id for item in items)
+ for key, value in docs:
+ await vector_store.aput(("test",), key, value)
- namespaces = await store.alist_namespaces(prefix=["test"])
- assert ("test", "documents") in namespaces
+ results = await vector_store.asearch(("test",), query="long text")
+ assert len(results) > 0
- await store.adelete(namespace, item_id)
- await store.adelete(namespace, new_item_id)
- deleted_item = await store.aget(namespace, item_id)
- assert deleted_item is None
+ doc_order = [r.key for r in results]
+ assert "doc2" in doc_order
+ assert "doc3" in doc_order
- deleted_item = await store.aget(namespace, new_item_id)
- assert deleted_item is None
- empty_search_results = await store.asearch(["test"], limit=10)
- assert len(empty_search_results) == 0
+async def test_vector_update_with_embedding(vector_store: AsyncPostgresStore) -> None:
+ """Test that updating items properly updates their embeddings."""
+ await vector_store.aput(("test",), "doc1", {"text": "zany zebra Xerxes"})
+ await vector_store.aput(("test",), "doc2", {"text": "something about dogs"})
+ await vector_store.aput(("test",), "doc3", {"text": "text about birds"})
- async def test_list_namespaces(self) -> None:
- async with AsyncPostgresStore.from_conn_string(DEFAULT_URI) as store:
- test_pref = str(uuid.uuid4())
- test_namespaces = [
- (test_pref, "test", "documents", "public", test_pref),
- (test_pref, "test", "documents", "private", test_pref),
- (test_pref, "test", "images", "public", test_pref),
- (test_pref, "test", "images", "private", test_pref),
- (test_pref, "prod", "documents", "public", test_pref),
- (
- test_pref,
- "prod",
- "documents",
- "some",
- "nesting",
- "public",
- test_pref,
- ),
- (test_pref, "prod", "documents", "private", test_pref),
- ]
+ results_initial = await vector_store.asearch(("test",), query="Zany Xerxes")
+ assert len(results_initial) > 0
+ assert results_initial[0].key == "doc1"
+ initial_score = results_initial[0].score
- for namespace in test_namespaces:
- await store.aput(namespace, "dummy", {"content": "dummy"})
+ await vector_store.aput(("test",), "doc1", {"text": "new text about dogs"})
- prefix_result = await store.alist_namespaces(prefix=[test_pref, "test"])
- assert len(prefix_result) == 4
- assert all([ns[1] == "test" for ns in prefix_result])
+ results_after = await vector_store.asearch(("test",), query="Zany Xerxes")
+ after_score = next((r.score for r in results_after if r.key == "doc1"), 0.0)
+ assert after_score < initial_score
- specific_prefix_result = await store.alist_namespaces(
- prefix=[test_pref, "test", "documents"]
- )
- assert len(specific_prefix_result) == 2
- assert all(
- [ns[1:3] == ("test", "documents") for ns in specific_prefix_result]
- )
+ results_new = await vector_store.asearch(("test",), query="new text about dogs")
+ for r in results_new:
+ if r.key == "doc1":
+ assert r.score > after_score
- suffix_result = await store.alist_namespaces(suffix=["public", test_pref])
- assert len(suffix_result) == 4
- assert all(ns[-2] == "public" for ns in suffix_result)
+ # Don't index this one
+ await vector_store.aput(
+ ("test",), "doc4", {"text": "new text about dogs"}, index=False
+ )
+ results_new = await vector_store.asearch(
+ ("test",), query="new text about dogs", limit=3
+ )
+ assert not any(r.key == "doc4" for r in results_new)
- prefix_suffix_result = await store.alist_namespaces(
- prefix=[test_pref, "test"], suffix=["public", test_pref]
- )
- assert len(prefix_suffix_result) == 2
- assert all(
- ns[1] == "test" and ns[-2] == "public" for ns in prefix_suffix_result
- )
- wildcard_prefix_result = await store.alist_namespaces(
- prefix=[test_pref, "*", "documents"]
- )
- assert len(wildcard_prefix_result) == 5
- assert all(ns[2] == "documents" for ns in wildcard_prefix_result)
+async def test_vector_search_with_filters(vector_store: AsyncPostgresStore) -> None:
+ """Test combining vector search with filters."""
+ docs = [
+ ("doc1", {"text": "red apple", "color": "red", "score": 4.5}),
+ ("doc2", {"text": "red car", "color": "red", "score": 3.0}),
+ ("doc3", {"text": "green apple", "color": "green", "score": 4.0}),
+ ("doc4", {"text": "blue car", "color": "blue", "score": 3.5}),
+ ]
- wildcard_suffix_result = await store.alist_namespaces(
- suffix=["*", "public", test_pref]
- )
- assert len(wildcard_suffix_result) == 4
- assert all(ns[-2] == "public" for ns in wildcard_suffix_result)
- wildcard_single = await store.alist_namespaces(
- suffix=["some", "*", "public", test_pref]
- )
- assert len(wildcard_single) == 1
- assert wildcard_single[0] == (
- test_pref,
- "prod",
- "documents",
- "some",
- "nesting",
- "public",
- test_pref,
- )
+ for key, value in docs:
+ await vector_store.aput(("test",), key, value)
- max_depth_result = await store.alist_namespaces(max_depth=3)
- assert all([len(ns) <= 3 for ns in max_depth_result])
- max_depth_result = await store.alist_namespaces(
- max_depth=4, prefix=[test_pref, "*", "documents"]
- )
- assert (
- len(set(tuple(res) for res in max_depth_result))
- == len(max_depth_result)
- == 5
- )
+ results = await vector_store.asearch(
+ ("test",), query="apple", filter={"color": "red"}
+ )
+ assert len(results) == 2
+ assert results[0].key == "doc1"
- limit_result = await store.alist_namespaces(prefix=[test_pref], limit=3)
- assert len(limit_result) == 3
+ results = await vector_store.asearch(
+ ("test",), query="car", filter={"color": "red"}
+ )
+ assert len(results) == 2
+ assert results[0].key == "doc2"
- offset_result = await store.alist_namespaces(prefix=[test_pref], offset=3)
- assert len(offset_result) == len(test_namespaces) - 3
+ results = await vector_store.asearch(
+ ("test",), query="bbbbluuu", filter={"score": {"$gt": 3.2}}
+ )
+ assert len(results) == 3
+ assert results[0].key == "doc4"
- empty_prefix_result = await store.alist_namespaces(prefix=[test_pref])
- assert len(empty_prefix_result) == len(test_namespaces)
- assert set(tuple(ns) for ns in empty_prefix_result) == set(
- tuple(ns) for ns in test_namespaces
- )
+ results = await vector_store.asearch(
+ ("test",), query="apple", filter={"score": {"$gte": 4.0}, "color": "green"}
+ )
+ assert len(results) == 1
+ assert results[0].key == "doc3"
- for namespace in test_namespaces:
- await store.adelete(namespace, "dummy")
- async def test_search(self):
- async with AsyncPostgresStore.from_conn_string(DEFAULT_URI) as store:
- test_namespaces = [
- ("test_search", "documents", "user1"),
- ("test_search", "documents", "user2"),
- ("test_search", "reports", "department1"),
- ("test_search", "reports", "department2"),
- ]
- test_items = [
- {"title": "Doc 1", "author": "John Doe", "tags": ["important"]},
- {"title": "Doc 2", "author": "Jane Smith", "tags": ["draft"]},
- {"title": "Report A", "author": "John Doe", "tags": ["final"]},
- {"title": "Report B", "author": "Alice Johnson", "tags": ["draft"]},
- ]
- empty = await store.asearch(
- (
- "scoped",
- "assistant_id",
- "shared",
- "6c5356f6-63ab-4158-868d-cd9fd14c736e",
- ),
- limit=10,
- offset=0,
- )
- assert len(empty) == 0
+async def test_vector_search_pagination(vector_store: AsyncPostgresStore) -> None:
+ """Test pagination with vector search."""
+ for i in range(5):
+ await vector_store.aput(
+ ("test",), f"doc{i}", {"text": f"test document number {i}"}
+ )
- for namespace, item in zip(test_namespaces, test_items):
- await store.aput(namespace, f"item_{namespace[-1]}", item)
+ results_page1 = await vector_store.asearch(("test",), query="test", limit=2)
+ results_page2 = await vector_store.asearch(
+ ("test",), query="test", limit=2, offset=2
+ )
- docs_result = await store.asearch(["test_search", "documents"])
- assert len(docs_result) == 2
- assert all([item.namespace[1] == "documents" for item in docs_result]), [
- item.namespace for item in docs_result
- ]
+ assert len(results_page1) == 2
+ assert len(results_page2) == 2
+ assert results_page1[0].key != results_page2[0].key
- reports_result = await store.asearch(["test_search", "reports"])
- assert len(reports_result) == 2
- assert all(item.namespace[1] == "reports" for item in reports_result)
+ all_results = await vector_store.asearch(("test",), query="test", limit=10)
+ assert len(all_results) == 5
- limited_result = await store.asearch(["test_search"], limit=2)
- assert len(limited_result) == 2
- offset_result = await store.asearch(["test_search"])
- assert len(offset_result) == 4
- offset_result = await store.asearch(["test_search"], offset=2)
- assert len(offset_result) == 2
- assert all(item not in limited_result for item in offset_result)
+async def test_vector_search_edge_cases(vector_store: AsyncPostgresStore) -> None:
+ """Test edge cases in vector search."""
+ await vector_store.aput(("test",), "doc1", {"text": "test document"})
- john_doe_result = await store.asearch(
- ["test_search"], filter={"author": "John Doe"}
- )
- assert len(john_doe_result) == 2
- assert all(item.value["author"] == "John Doe" for item in john_doe_result)
+ perfect_match = await vector_store.asearch(("test",), query="text test document")
+ perfect_score = perfect_match[0].score
- draft_result = await store.asearch(
- ["test_search"], filter={"tags": ["draft"]}
- )
- assert len(draft_result) == 2
- assert all("draft" in item.value["tags"] for item in draft_result)
+ results = await vector_store.asearch(("test",), query="")
+ assert len(results) == 1
+ assert results[0].score is None
- page1 = await store.asearch(["test_search"], limit=2, offset=0)
- page2 = await store.asearch(["test_search"], limit=2, offset=2)
- all_items = page1 + page2
- assert len(all_items) == 4
- assert len(set(item.key for item in all_items)) == 4
- empty = await store.asearch(
- (
- "scoped",
- "assistant_id",
- "shared",
- "again",
- "maybe",
- "some-long",
- "6be5cb0e-2eb4-42e6-bb6b-fba3c269db25",
- ),
- limit=10,
- offset=0,
- )
- assert len(empty) == 0
+ results = await vector_store.asearch(("test",), query=None)
+ assert len(results) == 1
+ assert results[0].score is None
- # Test with a namespace beginning with a number (like a UUID)
- uuid_namespace = (str(uuid.uuid4()), "documents")
- uuid_item_id = "uuid_doc"
- uuid_item_value = {
- "title": "UUID Document",
- "content": "This document has a UUID namespace.",
- }
+ long_query = "foo " * 100
+ results = await vector_store.asearch(("test",), query=long_query)
+ assert len(results) == 1
+ assert results[0].score < perfect_score
- # Insert the item with the UUID namespace
- await store.aput(uuid_namespace, uuid_item_id, uuid_item_value)
+ special_query = "test!@#$%^&*()"
+ results = await vector_store.asearch(("test",), query=special_query)
+ assert len(results) == 1
+ assert results[0].score < perfect_score
- # Retrieve the item to verify it was stored correctly
- retrieved_item = await store.aget(uuid_namespace, uuid_item_id)
- assert retrieved_item is not None
- assert retrieved_item.namespace == uuid_namespace
- assert retrieved_item.key == uuid_item_id
- assert retrieved_item.value == uuid_item_value
- # Search for the item using the UUID namespace
- search_result = await store.asearch([uuid_namespace[0]])
- assert len(search_result) == 1
- assert search_result[0].key == uuid_item_id
- assert search_result[0].value == uuid_item_value
+@pytest.mark.parametrize(
+ "vector_type,distance_type",
+ [
+ *itertools.product(["vector", "halfvec"], ["cosine", "inner_product", "l2"]),
+ ],
+)
+async def test_embed_with_path(
+ request: Any,
+ fake_embeddings: CharacterEmbeddings,
+ vector_type: str,
+ distance_type: str,
+) -> None:
+ """Test vector search with specific text fields in Postgres store."""
+ async with _create_vector_store(
+ vector_type,
+ distance_type,
+ fake_embeddings,
+ text_fields=["key0", "key1", "key3"],
+ ) as store:
+ # This will have 2 vectors representing it
+ doc1 = {
+ # Omit key0 - check it doesn't raise an error
+ "key1": "xxx",
+ "key2": "yyy",
+ "key3": "zzz",
+ }
+ # This will have 3 vectors representing it
+ doc2 = {
+ "key0": "uuu",
+ "key1": "vvv",
+ "key2": "www",
+ "key3": "xxx",
+ }
+ await store.aput(("test",), "doc1", doc1)
+ await store.aput(("test",), "doc2", doc2)
- # Clean up: delete the item with the UUID namespace
- await store.adelete(uuid_namespace, uuid_item_id)
+ # doc2.key3 and doc1.key1 both would have the highest score
+ results = await store.asearch(("test",), query="xxx")
+ assert len(results) == 2
+ assert results[0].key != results[1].key
+ ascore = results[0].score
+ bscore = results[1].score
+ assert ascore == pytest.approx(bscore, abs=1e-3)
- # Verify the item was deleted
- deleted_item = await store.aget(uuid_namespace, uuid_item_id)
- assert deleted_item is None
+ results = await store.asearch(("test",), query="uuu")
+ assert len(results) == 2
+ assert results[0].key != results[1].key
+ assert results[0].key == "doc2"
+ assert results[0].score > results[1].score
+ assert ascore == pytest.approx(results[0].score, abs=1e-3)
- for namespace in test_namespaces:
- await store.adelete(namespace, f"item_{namespace[-1]}")
+ # Un-indexed - will have low results for both. Not zero (because we're projecting)
+ # but less than the above.
+ results = await store.asearch(("test",), query="www")
+ assert len(results) == 2
+ assert results[0].score < ascore
+ assert results[1].score < ascore
+
+
+@pytest.mark.parametrize(
+ "vector_type,distance_type",
+ [
+ *itertools.product(["vector", "halfvec"], ["cosine", "inner_product", "l2"]),
+ ],
+)
+async def test_search_sorting(
+ request: Any,
+ fake_embeddings: CharacterEmbeddings,
+ vector_type: str,
+ distance_type: str,
+) -> None:
+ """Test operation-level field configuration for vector search."""
+ async with _create_vector_store(
+ vector_type,
+ distance_type,
+ fake_embeddings,
+ text_fields=["key1"], # Default fields that won't match our test data
+ ) as store:
+ amatch = {
+ "key1": "mmm",
+ }
+
+ await store.aput(("test", "M"), "M", amatch)
+ N = 100
+ for i in range(N):
+ await store.aput(("test", "A"), f"A{i}", {"key1": "no"})
+ for i in range(N):
+ await store.aput(("test", "Z"), f"Z{i}", {"key1": "no"})
+
+ results = await store.asearch(("test",), query="mmm", limit=10)
+ assert len(results) == 10
+ assert len(set(r.key for r in results)) == 10
+ assert results[0].key == "M"
+ assert results[0].score > results[1].score
diff --git a/libs/checkpoint-postgres/tests/test_store.py b/libs/checkpoint-postgres/tests/test_store.py
index 645c37e9f..c9d220fe0 100644
--- a/libs/checkpoint-postgres/tests/test_store.py
+++ b/libs/checkpoint-postgres/tests/test_store.py
@@ -1,9 +1,11 @@
# type: ignore
+from contextlib import contextmanager
+from typing import Any, Optional
from uuid import uuid4
import pytest
-from conftest import DEFAULT_URI # type: ignore
+from langchain_core.embeddings import Embeddings
from psycopg import Connection
from langgraph.store.base import (
@@ -15,6 +17,11 @@ from langgraph.store.base import (
SearchOp,
)
from langgraph.store.postgres import PostgresStore
+from tests.conftest import (
+ DEFAULT_URI,
+ VECTOR_TYPES,
+ CharacterEmbeddings,
+)
@pytest.fixture(scope="function", params=["default", "pipe", "pool"])
@@ -340,3 +347,351 @@ class TestPostgresStore:
# Cleanup
for namespace, key, _ in test_data:
store.delete(namespace, key)
+
+
+@contextmanager
+def _create_vector_store(
+ vector_type: str,
+ distance_type: str,
+ fake_embeddings: Embeddings,
+ text_fields: Optional[list[str]] = None,
+) -> PostgresStore:
+ """Create a store with vector search enabled."""
+ database = f"test_{uuid4().hex[:16]}"
+ uri_parts = DEFAULT_URI.split("/")
+ uri_base = "/".join(uri_parts[:-1])
+ query_params = ""
+ if "?" in uri_parts[-1]:
+ db_name, query_params = uri_parts[-1].split("?", 1)
+ query_params = "?" + query_params
+
+ conn_string = f"{uri_base}/{database}{query_params}"
+ admin_conn_string = DEFAULT_URI
+
+ index_config = {
+ "dims": fake_embeddings.dims,
+ "embed": fake_embeddings,
+ "ann_index_config": {
+ "vector_type": vector_type,
+ },
+ "distance_type": distance_type,
+ "text_fields": text_fields,
+ }
+
+ with Connection.connect(admin_conn_string, autocommit=True) as conn:
+ conn.execute(f"CREATE DATABASE {database}")
+ try:
+ with PostgresStore.from_conn_string(
+ conn_string,
+ index=index_config,
+ ) as store:
+ store.setup()
+ yield store
+ finally:
+ with Connection.connect(admin_conn_string, autocommit=True) as conn:
+ conn.execute(f"DROP DATABASE {database}")
+
+
+@pytest.fixture(
+ scope="function",
+ params=[
+ (vector_type, distance_type)
+ for vector_type in VECTOR_TYPES
+ for distance_type in (
+ ["hamming"] if vector_type == "bit" else ["l2", "inner_product", "cosine"]
+ )
+ ],
+ ids=lambda p: f"{p[0]}_{p[1]}",
+)
+def vector_store(
+ request,
+ fake_embeddings: Embeddings,
+) -> PostgresStore:
+ """Create a store with vector search enabled."""
+ vector_type, distance_type = request.param
+ with _create_vector_store(vector_type, distance_type, fake_embeddings) as store:
+ yield store
+
+
+def test_vector_store_initialization(
+ vector_store: PostgresStore, fake_embeddings: CharacterEmbeddings
+) -> None:
+ """Test store initialization with embedding config."""
+ # Store should be initialized with embedding config
+ assert vector_store.index_config is not None
+ assert vector_store.index_config["dims"] == fake_embeddings.dims
+ assert vector_store.index_config["embed"] == fake_embeddings
+
+
+def test_vector_insert_with_auto_embedding(vector_store: PostgresStore) -> None:
+ """Test inserting items that get auto-embedded."""
+ docs = [
+ ("doc1", {"text": "short text"}),
+ ("doc2", {"text": "longer text document"}),
+ ("doc3", {"text": "longest text document here"}),
+ ("doc4", {"description": "text in description field"}),
+ ("doc5", {"content": "text in content field"}),
+ ("doc6", {"body": "text in body field"}),
+ ]
+
+ for key, value in docs:
+ vector_store.put(("test",), key, value)
+
+ results = vector_store.search(("test",), query="long text")
+ assert len(results) > 0
+
+ doc_order = [r.key for r in results]
+ assert "doc2" in doc_order
+ assert "doc3" in doc_order
+
+
+def test_vector_update_with_embedding(vector_store: PostgresStore) -> None:
+ """Test that updating items properly updates their embeddings."""
+ vector_store.put(("test",), "doc1", {"text": "zany zebra Xerxes"})
+ vector_store.put(("test",), "doc2", {"text": "something about dogs"})
+ vector_store.put(("test",), "doc3", {"text": "text about birds"})
+
+ results_initial = vector_store.search(("test",), query="Zany Xerxes")
+ assert len(results_initial) > 0
+ assert results_initial[0].key == "doc1"
+ initial_score = results_initial[0].score
+
+ vector_store.put(("test",), "doc1", {"text": "new text about dogs"})
+
+ results_after = vector_store.search(("test",), query="Zany Xerxes")
+ after_score = next((r.score for r in results_after if r.key == "doc1"), 0.0)
+ assert after_score < initial_score
+
+ results_new = vector_store.search(("test",), query="new text about dogs")
+ for r in results_new:
+ if r.key == "doc1":
+ assert r.score > after_score
+
+ # Don't index this one
+ vector_store.put(("test",), "doc4", {"text": "new text about dogs"}, index=False)
+ results_new = vector_store.search(("test",), query="new text about dogs", limit=3)
+ assert not any(r.key == "doc4" for r in results_new)
+
+
+def test_vector_search_with_filters(vector_store: PostgresStore) -> None:
+ """Test combining vector search with filters."""
+ # Insert test documents
+ docs = [
+ ("doc1", {"text": "red apple", "color": "red", "score": 4.5}),
+ ("doc2", {"text": "red car", "color": "red", "score": 3.0}),
+ ("doc3", {"text": "green apple", "color": "green", "score": 4.0}),
+ ("doc4", {"text": "blue car", "color": "blue", "score": 3.5}),
+ ]
+
+ for key, value in docs:
+ vector_store.put(("test",), key, value)
+
+ results = vector_store.search(("test",), query="apple", filter={"color": "red"})
+ assert len(results) == 2
+ assert results[0].key == "doc1"
+
+ results = vector_store.search(("test",), query="car", filter={"color": "red"})
+ assert len(results) == 2
+ assert results[0].key == "doc2"
+
+ results = vector_store.search(
+ ("test",), query="bbbbluuu", filter={"score": {"$gt": 3.2}}
+ )
+ assert len(results) == 3
+ assert results[0].key == "doc4"
+
+ # Multiple filters
+ results = vector_store.search(
+ ("test",), query="apple", filter={"score": {"$gte": 4.0}, "color": "green"}
+ )
+ assert len(results) == 1
+ assert results[0].key == "doc3"
+
+
+def test_vector_search_pagination(vector_store: PostgresStore) -> None:
+ """Test pagination with vector search."""
+ # Insert multiple similar documents
+ for i in range(5):
+ vector_store.put(("test",), f"doc{i}", {"text": f"test document number {i}"})
+
+ # Test with different page sizes
+ results_page1 = vector_store.search(("test",), query="test", limit=2)
+ results_page2 = vector_store.search(("test",), query="test", limit=2, offset=2)
+
+ assert len(results_page1) == 2
+ assert len(results_page2) == 2
+ assert results_page1[0].key != results_page2[0].key
+
+ # Get all results
+ all_results = vector_store.search(("test",), query="test", limit=10)
+ assert len(all_results) == 5
+
+
+def test_vector_search_edge_cases(vector_store: PostgresStore) -> None:
+ """Test edge cases in vector search."""
+ vector_store.put(("test",), "doc1", {"text": "test document"})
+
+ results = vector_store.search(("test",), query="")
+ assert len(results) == 1
+
+ results = vector_store.search(("test",), query=None)
+ assert len(results) == 1
+
+ long_query = "test " * 100
+ results = vector_store.search(("test",), query=long_query)
+ assert len(results) == 1
+
+ special_query = "test!@#$%^&*()"
+ results = vector_store.search(("test",), query=special_query)
+ assert len(results) == 1
+
+
+@pytest.mark.parametrize(
+ "vector_type,distance_type",
+ [
+ ("vector", "cosine"),
+ ("vector", "inner_product"),
+ ("halfvec", "cosine"),
+ ("halfvec", "inner_product"),
+ ],
+)
+def test_embed_with_path_sync(
+ request: Any,
+ fake_embeddings: CharacterEmbeddings,
+ vector_type: str,
+ distance_type: str,
+) -> None:
+ """Test vector search with specific text fields in Postgres store."""
+ with _create_vector_store(
+ vector_type,
+ distance_type,
+ fake_embeddings,
+ text_fields=["key0", "key1", "key3"],
+ ) as store:
+ # This will have 2 vectors representing it
+ doc1 = {
+ # Omit key0 - check it doesn't raise an error
+ "key1": "xxx",
+ "key2": "yyy",
+ "key3": "zzz",
+ }
+ # This will have 3 vectors representing it
+ doc2 = {
+ "key0": "uuu",
+ "key1": "vvv",
+ "key2": "www",
+ "key3": "xxx",
+ }
+ store.put(("test",), "doc1", doc1)
+ store.put(("test",), "doc2", doc2)
+
+ # doc2.key3 and doc1.key1 both would have the highest score
+ results = store.search(("test",), query="xxx")
+ assert len(results) == 2
+ assert results[0].key != results[1].key
+ ascore = results[0].score
+ bscore = results[1].score
+ assert ascore == pytest.approx(bscore, abs=1e-3)
+
+ # ~Only match doc2
+ results = store.search(("test",), query="uuu")
+ assert len(results) == 2
+ assert results[0].key != results[1].key
+ assert results[0].key == "doc2"
+ assert results[0].score > results[1].score
+ assert ascore == pytest.approx(results[0].score, abs=1e-3)
+
+ # ~Only match doc1
+ results = store.search(("test",), query="zzz")
+ assert len(results) == 2
+ assert results[0].key != results[1].key
+ assert results[0].key == "doc1"
+ assert results[0].score > results[1].score
+ assert ascore == pytest.approx(results[0].score, abs=1e-3)
+
+ # Un-indexed - will have low results for both. Not zero (because we're projecting)
+ # but less than the above.
+ results = store.search(("test",), query="www")
+ assert len(results) == 2
+ assert results[0].key != results[1].key
+ assert results[0].score < ascore
+ assert results[1].score < ascore
+
+
+@pytest.mark.parametrize(
+ "vector_type,distance_type",
+ [
+ ("vector", "cosine"),
+ ("vector", "inner_product"),
+ ("halfvec", "cosine"),
+ ("halfvec", "inner_product"),
+ ],
+)
+def test_embed_with_path_operation_config(
+ request: Any,
+ fake_embeddings: CharacterEmbeddings,
+ vector_type: str,
+ distance_type: str,
+) -> None:
+ """Test operation-level field configuration for vector search."""
+ with _create_vector_store(
+ vector_type,
+ distance_type,
+ fake_embeddings,
+ text_fields=["key17"], # Default fields that won't match our test data
+ ) as store:
+ doc3 = {
+ "key0": "aaa",
+ "key1": "bbb",
+ "key2": "ccc",
+ "key3": "ddd",
+ }
+ doc4 = {
+ "key0": "eee",
+ "key1": "bbb", # Same as doc3.key1
+ "key2": "fff",
+ "key3": "ggg",
+ }
+
+ store.put(("test",), "doc3", doc3, index=["key0", "key1"])
+ store.put(("test",), "doc4", doc4, index=["key1", "key3"])
+
+ results = store.search(("test",), query="aaa")
+ assert len(results) == 2
+ assert results[0].key == "doc3"
+ assert len(set(r.key for r in results)) == 2
+ assert results[0].score > results[1].score
+
+ results = store.search(("test",), query="ggg")
+ assert len(results) == 2
+ assert results[0].key == "doc4"
+ assert results[0].score > results[1].score
+
+ results = store.search(("test",), query="bbb")
+ assert len(results) == 2
+ assert results[0].key != results[1].key
+ assert results[0].score == pytest.approx(results[1].score, abs=1e-3)
+
+ results = store.search(("test",), query="ccc")
+ assert len(results) == 2
+ assert all(
+ r.score < 0.9 for r in results
+ ) # Unindexed field should have low scores
+
+ # Test index=False behavior
+ doc5 = {
+ "key0": "hhh",
+ "key1": "iii",
+ }
+ store.put(("test",), "doc5", doc5, index=False)
+ results = store.search(("test",))
+ assert len(results) == 3
+ assert all(r.score is None for r in results)
+ assert any(r.key == "doc5" for r in results)
+
+ results = store.search(("test",), query="hhh")
+ # TODO: We don't currently fill in additional results if there are not enough
+ # returned during vector search.
+ # assert len(results) == 3
+ # doc5_result = next(r for r in results if r.key == "doc5")
+ # assert doc5_result.score is None
diff --git a/libs/checkpoint-postgres/tests/test_sync.py b/libs/checkpoint-postgres/tests/test_sync.py
index ced755955..052e699b3 100644
--- a/libs/checkpoint-postgres/tests/test_sync.py
+++ b/libs/checkpoint-postgres/tests/test_sync.py
@@ -1,7 +1,6 @@
from typing import Any
import pytest
-from conftest import DEFAULT_URI # type: ignore
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
@@ -11,6 +10,7 @@ from langgraph.checkpoint.base import (
empty_checkpoint,
)
from langgraph.checkpoint.postgres import PostgresSaver
+from tests.conftest import DEFAULT_URI
class TestPostgresSaver:
diff --git a/libs/checkpoint/pyproject.toml b/libs/checkpoint/pyproject.toml
index deb7de5c4..278594fcb 100644
--- a/libs/checkpoint/pyproject.toml
+++ b/libs/checkpoint/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
-version = "2.0.5"
+version = "2.0.6"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
From 855a3d21ffd174963cbd8f483fb63d7082245513 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 27 Nov 2024 20:50:11 -0800
Subject: [PATCH 069/149] Update Checkpoint Version (#2565)
---
libs/checkpoint/pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/checkpoint/pyproject.toml b/libs/checkpoint/pyproject.toml
index 278594fcb..ef12f2052 100644
--- a/libs/checkpoint/pyproject.toml
+++ b/libs/checkpoint/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
-version = "2.0.6"
+version = "2.0.7"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
From c87f9ab6b12be70e8421fa9fa3cd995b037454e3 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 27 Nov 2024 22:31:03 -0800
Subject: [PATCH 070/149] Fix sentence fragment (#2566)
---
docs/docs/tutorials/introduction.ipynb | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/docs/docs/tutorials/introduction.ipynb b/docs/docs/tutorials/introduction.ipynb
index 119fbf861..2139b6d46 100644
--- a/docs/docs/tutorials/introduction.ipynb
+++ b/docs/docs/tutorials/introduction.ipynb
@@ -19,7 +19,7 @@
"\n",
"## Setup\n",
"\n",
- "First, install the required packages:"
+ "First, install the required packages and configure your environment:"
]
},
{
@@ -33,14 +33,6 @@
"%pip install -U langgraph langsmith langchain_anthropic"
]
},
- {
- "cell_type": "markdown",
- "id": "a6d1e870-1bc0-4d44-86c0-96681ccf6113",
- "metadata": {},
- "source": [
- "In this tutorial, we'll be "
- ]
- },
{
"cell_type": "code",
"execution_count": 2,
From 12486d977a565b867cd003e25dc6c7ce22ba65b1 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 27 Nov 2024 22:31:16 -0800
Subject: [PATCH 071/149] Update postgres-checkpoint min bounds (#2564)
---
libs/checkpoint-postgres/poetry.lock | 955 +++++++++++++-----------
libs/checkpoint-postgres/pyproject.toml | 2 +-
2 files changed, 530 insertions(+), 427 deletions(-)
diff --git a/libs/checkpoint-postgres/poetry.lock b/libs/checkpoint-postgres/poetry.lock
index b57babc0c..a3c23df97 100644
--- a/libs/checkpoint-postgres/poetry.lock
+++ b/libs/checkpoint-postgres/poetry.lock
@@ -13,13 +13,13 @@ files = [
[[package]]
name = "anyio"
-version = "4.4.0"
+version = "4.6.2.post1"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
files = [
- {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
- {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
+ {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"},
+ {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"},
]
[package.dependencies]
@@ -29,118 +29,133 @@ sniffio = ">=1.1"
typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
[package.extras]
-doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
-test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
-trio = ["trio (>=0.23)"]
+doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
+test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"]
+trio = ["trio (>=0.26.1)"]
[[package]]
name = "certifi"
-version = "2024.7.4"
+version = "2024.8.30"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
- {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
- {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
+ {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
+ {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
]
[[package]]
name = "charset-normalizer"
-version = "3.3.2"
+version = "3.4.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
files = [
- {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
- {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
+ {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"},
+ {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"},
+ {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"},
+ {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"},
+ {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"},
+ {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"},
+ {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"},
+ {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"},
+ {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"},
+ {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"},
+ {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"},
+ {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"},
+ {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"},
+ {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"},
+ {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"},
+ {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"},
+ {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"},
+ {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"},
+ {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"},
+ {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"},
+ {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"},
+ {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"},
+ {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"},
+ {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"},
+ {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"},
+ {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"},
+ {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"},
+ {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"},
+ {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"},
+ {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"},
+ {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"},
+ {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"},
+ {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"},
+ {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"},
+ {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"},
+ {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"},
+ {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"},
+ {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"},
+ {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"},
+ {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"},
+ {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"},
+ {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"},
+ {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"},
+ {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"},
+ {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"},
+ {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"},
+ {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"},
+ {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"},
+ {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"},
+ {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"},
+ {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"},
+ {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"},
+ {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"},
+ {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"},
+ {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"},
+ {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"},
+ {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"},
+ {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"},
+ {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"},
+ {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"},
]
[[package]]
@@ -208,13 +223,13 @@ files = [
[[package]]
name = "httpcore"
-version = "1.0.5"
+version = "1.0.7"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
files = [
- {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
- {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
+ {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"},
+ {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"},
]
[package.dependencies]
@@ -225,7 +240,7 @@ h11 = ">=0.13,<0.15"
asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
-trio = ["trio (>=0.22.0,<0.26.0)"]
+trio = ["trio (>=0.22.0,<1.0)"]
[[package]]
name = "httpx"
@@ -254,15 +269,18 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "idna"
-version = "3.7"
+version = "3.10"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
files = [
- {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
- {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
+ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
+ {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
]
+[package.extras]
+all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
+
[[package]]
name = "iniconfig"
version = "2.0.0"
@@ -301,30 +319,30 @@ files = [
[[package]]
name = "langchain-core"
-version = "0.3.0"
+version = "0.3.21"
description = "Building applications with LLMs through composability"
optional = false
python-versions = "<4.0,>=3.9"
files = [
- {file = "langchain_core-0.3.0-py3-none-any.whl", hash = "sha256:bee6dae2366d037ef0c5b87401fed14b5497cad26f97724e8c9ca7bc9239e847"},
- {file = "langchain_core-0.3.0.tar.gz", hash = "sha256:1249149ea3ba24c9c761011483c14091573a5eb1a773aa0db9c8ad155dd4a69d"},
+ {file = "langchain_core-0.3.21-py3-none-any.whl", hash = "sha256:7e723dff80946a1198976c6876fea8326dc82566ef9bcb5f8d9188f738733665"},
+ {file = "langchain_core-0.3.21.tar.gz", hash = "sha256:561b52b258ffa50a9fb11d7a1940ebfd915654d1ec95b35e81dfd5ee84143411"},
]
[package.dependencies]
jsonpatch = ">=1.33,<2.0"
-langsmith = ">=0.1.117,<0.2.0"
+langsmith = ">=0.1.125,<0.2.0"
packaging = ">=23.2,<25"
pydantic = [
{version = ">=2.5.2,<3.0.0", markers = "python_full_version < \"3.12.4\""},
{version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""},
]
PyYAML = ">=5.3"
-tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0"
+tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0"
typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
-version = "2.0.2"
+version = "2.0.7"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -341,23 +359,27 @@ url = "../checkpoint"
[[package]]
name = "langsmith"
-version = "0.1.120"
+version = "0.1.147"
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
optional = false
python-versions = "<4.0,>=3.8.1"
files = [
- {file = "langsmith-0.1.120-py3-none-any.whl", hash = "sha256:54d2785e301646c0988e0a69ebe4d976488c87b41928b358cb153b6ddd8db62b"},
- {file = "langsmith-0.1.120.tar.gz", hash = "sha256:25499ca187b41bd89d784b272b97a8d76f60e0e21bdf20336e8a2aa6a9b23ac9"},
+ {file = "langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15"},
+ {file = "langsmith-0.1.147.tar.gz", hash = "sha256:2e933220318a4e73034657103b3b1a3a6109cc5db3566a7e8e03be8d6d7def7a"},
]
[package.dependencies]
httpx = ">=0.23.0,<1"
-orjson = ">=3.9.14,<4.0.0"
+orjson = {version = ">=3.9.14,<4.0.0", markers = "platform_python_implementation != \"PyPy\""}
pydantic = [
{version = ">=1,<3", markers = "python_full_version < \"3.12.4\""},
{version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""},
]
requests = ">=2,<3"
+requests-toolbelt = ">=1.0.0,<2.0.0"
+
+[package.extras]
+langsmith-pyo3 = ["langsmith-pyo3 (>=0.1.0rc2,<0.2.0)"]
[[package]]
name = "msgpack"
@@ -434,38 +456,43 @@ files = [
[[package]]
name = "mypy"
-version = "1.11.2"
+version = "1.13.0"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
files = [
- {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"},
- {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"},
- {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"},
- {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"},
- {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"},
- {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"},
- {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"},
- {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"},
- {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"},
- {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"},
- {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"},
- {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"},
- {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"},
- {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"},
- {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"},
- {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"},
- {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"},
- {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"},
- {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"},
- {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"},
- {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"},
- {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"},
- {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"},
- {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"},
- {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"},
- {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"},
- {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"},
+ {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"},
+ {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"},
+ {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"},
+ {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"},
+ {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"},
+ {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"},
+ {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"},
+ {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"},
+ {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"},
+ {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"},
+ {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"},
+ {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"},
+ {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"},
+ {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"},
+ {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"},
+ {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"},
+ {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"},
+ {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"},
+ {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"},
+ {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"},
+ {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"},
+ {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"},
+ {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"},
+ {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"},
+ {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"},
+ {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"},
+ {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"},
+ {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"},
+ {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"},
+ {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"},
+ {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"},
+ {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"},
]
[package.dependencies]
@@ -475,6 +502,7 @@ typing-extensions = ">=4.6.0"
[package.extras]
dmypy = ["psutil (>=4.0)"]
+faster-cache = ["orjson"]
install-types = ["pip"]
mypyc = ["setuptools (>=50)"]
reports = ["lxml"]
@@ -492,75 +520,97 @@ files = [
[[package]]
name = "orjson"
-version = "3.10.6"
+version = "3.10.12"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.8"
files = [
- {file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"},
- {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"},
- {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eadc8fd310edb4bdbd333374f2c8fec6794bbbae99b592f448d8214a5e4050c0"},
- {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61272a5aec2b2661f4fa2b37c907ce9701e821b2c1285d5c3ab0207ebd358d38"},
- {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57985ee7e91d6214c837936dc1608f40f330a6b88bb13f5a57ce5257807da143"},
- {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:633a3b31d9d7c9f02d49c4ab4d0a86065c4a6f6adc297d63d272e043472acab5"},
- {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1c680b269d33ec444afe2bdc647c9eb73166fa47a16d9a75ee56a374f4a45f43"},
- {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f759503a97a6ace19e55461395ab0d618b5a117e8d0fbb20e70cfd68a47327f2"},
- {file = "orjson-3.10.6-cp310-none-win32.whl", hash = "sha256:95a0cce17f969fb5391762e5719575217bd10ac5a189d1979442ee54456393f3"},
- {file = "orjson-3.10.6-cp310-none-win_amd64.whl", hash = "sha256:df25d9271270ba2133cc88ee83c318372bdc0f2cd6f32e7a450809a111efc45c"},
- {file = "orjson-3.10.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b1ec490e10d2a77c345def52599311849fc063ae0e67cf4f84528073152bb2ba"},
- {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d43d3feb8f19d07e9f01e5b9be4f28801cf7c60d0fa0d279951b18fae1932b"},
- {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3045267e98fe749408eee1593a142e02357c5c99be0802185ef2170086a863"},
- {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27bc6a28ae95923350ab382c57113abd38f3928af3c80be6f2ba7eb8d8db0b0"},
- {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d27456491ca79532d11e507cadca37fb8c9324a3976294f68fb1eff2dc6ced5a"},
- {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05ac3d3916023745aa3b3b388e91b9166be1ca02b7c7e41045da6d12985685f0"},
- {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1335d4ef59ab85cab66fe73fd7a4e881c298ee7f63ede918b7faa1b27cbe5212"},
- {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4bbc6d0af24c1575edc79994c20e1b29e6fb3c6a570371306db0993ecf144dc5"},
- {file = "orjson-3.10.6-cp311-none-win32.whl", hash = "sha256:450e39ab1f7694465060a0550b3f6d328d20297bf2e06aa947b97c21e5241fbd"},
- {file = "orjson-3.10.6-cp311-none-win_amd64.whl", hash = "sha256:227df19441372610b20e05bdb906e1742ec2ad7a66ac8350dcfd29a63014a83b"},
- {file = "orjson-3.10.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ea2977b21f8d5d9b758bb3f344a75e55ca78e3ff85595d248eee813ae23ecdfb"},
- {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6f3d167d13a16ed263b52dbfedff52c962bfd3d270b46b7518365bcc2121eed"},
- {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f710f346e4c44a4e8bdf23daa974faede58f83334289df80bc9cd12fe82573c7"},
- {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7275664f84e027dcb1ad5200b8b18373e9c669b2a9ec33d410c40f5ccf4b257e"},
- {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0943e4c701196b23c240b3d10ed8ecd674f03089198cf503105b474a4f77f21f"},
- {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:446dee5a491b5bc7d8f825d80d9637e7af43f86a331207b9c9610e2f93fee22a"},
- {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:64c81456d2a050d380786413786b057983892db105516639cb5d3ee3c7fd5148"},
- {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"},
- {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"},
- {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"},
- {file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"},
- {file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"},
- {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"},
- {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"},
- {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"},
- {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2c116072a8533f2fec435fde4d134610f806bdac20188c7bd2081f3e9e0133f"},
- {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6eeb13218c8cf34c61912e9df2de2853f1d009de0e46ea09ccdf3d757896af0a"},
- {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965a916373382674e323c957d560b953d81d7a8603fbeee26f7b8248638bd48b"},
- {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03c95484d53ed8e479cade8628c9cea00fd9d67f5554764a1110e0d5aa2de96e"},
- {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e060748a04cccf1e0a6f2358dffea9c080b849a4a68c28b1b907f272b5127e9b"},
- {file = "orjson-3.10.6-cp38-none-win32.whl", hash = "sha256:738dbe3ef909c4b019d69afc19caf6b5ed0e2f1c786b5d6215fbb7539246e4c6"},
- {file = "orjson-3.10.6-cp38-none-win_amd64.whl", hash = "sha256:d40f839dddf6a7d77114fe6b8a70218556408c71d4d6e29413bb5f150a692ff7"},
- {file = "orjson-3.10.6-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:697a35a083c4f834807a6232b3e62c8b280f7a44ad0b759fd4dce748951e70db"},
- {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd502f96bf5ea9a61cbc0b2b5900d0dd68aa0da197179042bdd2be67e51a1e4b"},
- {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f215789fb1667cdc874c1b8af6a84dc939fd802bf293a8334fce185c79cd359b"},
- {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2debd8ddce948a8c0938c8c93ade191d2f4ba4649a54302a7da905a81f00b56"},
- {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5410111d7b6681d4b0d65e0f58a13be588d01b473822483f77f513c7f93bd3b2"},
- {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1f28a137337fdc18384079fa5726810681055b32b92253fa15ae5656e1dddb"},
- {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bf2fbbce5fe7cd1aa177ea3eab2b8e6a6bc6e8592e4279ed3db2d62e57c0e1b2"},
- {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:79b9b9e33bd4c517445a62b90ca0cc279b0f1f3970655c3df9e608bc3f91741a"},
- {file = "orjson-3.10.6-cp39-none-win32.whl", hash = "sha256:30b0a09a2014e621b1adf66a4f705f0809358350a757508ee80209b2d8dae219"},
- {file = "orjson-3.10.6-cp39-none-win_amd64.whl", hash = "sha256:49e3bc615652617d463069f91b867a4458114c5b104e13b7ae6872e5f79d0844"},
- {file = "orjson-3.10.6.tar.gz", hash = "sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7"},
+ {file = "orjson-3.10.12-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ece01a7ec71d9940cc654c482907a6b65df27251255097629d0dea781f255c6d"},
+ {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34ec9aebc04f11f4b978dd6caf697a2df2dd9b47d35aa4cc606cabcb9df69d7"},
+ {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd6ec8658da3480939c79b9e9e27e0db31dffcd4ba69c334e98c9976ac29140e"},
+ {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17e6baf4cf01534c9de8a16c0c611f3d94925d1701bf5f4aff17003677d8ced"},
+ {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6402ebb74a14ef96f94a868569f5dccf70d791de49feb73180eb3c6fda2ade56"},
+ {file = "orjson-3.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0000758ae7c7853e0a4a6063f534c61656ebff644391e1f81698c1b2d2fc8cd2"},
+ {file = "orjson-3.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:888442dcee99fd1e5bd37a4abb94930915ca6af4db50e23e746cdf4d1e63db13"},
+ {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c1f7a3ce79246aa0e92f5458d86c54f257fb5dfdc14a192651ba7ec2c00f8a05"},
+ {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:802a3935f45605c66fb4a586488a38af63cb37aaad1c1d94c982c40dcc452e85"},
+ {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1da1ef0113a2be19bb6c557fb0ec2d79c92ebd2fed4cfb1b26bab93f021fb885"},
+ {file = "orjson-3.10.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a3273e99f367f137d5b3fecb5e9f45bcdbfac2a8b2f32fbc72129bbd48789c2"},
+ {file = "orjson-3.10.12-cp310-none-win32.whl", hash = "sha256:475661bf249fd7907d9b0a2a2421b4e684355a77ceef85b8352439a9163418c3"},
+ {file = "orjson-3.10.12-cp310-none-win_amd64.whl", hash = "sha256:87251dc1fb2b9e5ab91ce65d8f4caf21910d99ba8fb24b49fd0c118b2362d509"},
+ {file = "orjson-3.10.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a734c62efa42e7df94926d70fe7d37621c783dea9f707a98cdea796964d4cf74"},
+ {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:750f8b27259d3409eda8350c2919a58b0cfcd2054ddc1bd317a643afc646ef23"},
+ {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb52c22bfffe2857e7aa13b4622afd0dd9d16ea7cc65fd2bf318d3223b1b6252"},
+ {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:440d9a337ac8c199ff8251e100c62e9488924c92852362cd27af0e67308c16ef"},
+ {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e15c06491c69997dfa067369baab3bf094ecb74be9912bdc4339972323f252"},
+ {file = "orjson-3.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:362d204ad4b0b8724cf370d0cd917bb2dc913c394030da748a3bb632445ce7c4"},
+ {file = "orjson-3.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b57cbb4031153db37b41622eac67329c7810e5f480fda4cfd30542186f006ae"},
+ {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:165c89b53ef03ce0d7c59ca5c82fa65fe13ddf52eeb22e859e58c237d4e33b9b"},
+ {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5dee91b8dfd54557c1a1596eb90bcd47dbcd26b0baaed919e6861f076583e9da"},
+ {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a4e1cfb72de6f905bdff061172adfb3caf7a4578ebf481d8f0530879476c07"},
+ {file = "orjson-3.10.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:038d42c7bc0606443459b8fe2d1f121db474c49067d8d14c6a075bbea8bf14dd"},
+ {file = "orjson-3.10.12-cp311-none-win32.whl", hash = "sha256:03b553c02ab39bed249bedd4abe37b2118324d1674e639b33fab3d1dafdf4d79"},
+ {file = "orjson-3.10.12-cp311-none-win_amd64.whl", hash = "sha256:8b8713b9e46a45b2af6b96f559bfb13b1e02006f4242c156cbadef27800a55a8"},
+ {file = "orjson-3.10.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53206d72eb656ca5ac7d3a7141e83c5bbd3ac30d5eccfe019409177a57634b0d"},
+ {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8010afc2150d417ebda810e8df08dd3f544e0dd2acab5370cfa6bcc0662f8f"},
+ {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed459b46012ae950dd2e17150e838ab08215421487371fa79d0eced8d1461d70"},
+ {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dcb9673f108a93c1b52bfc51b0af422c2d08d4fc710ce9c839faad25020bb69"},
+ {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22a51ae77680c5c4652ebc63a83d5255ac7d65582891d9424b566fb3b5375ee9"},
+ {file = "orjson-3.10.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910fdf2ac0637b9a77d1aad65f803bac414f0b06f720073438a7bd8906298192"},
+ {file = "orjson-3.10.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24ce85f7100160936bc2116c09d1a8492639418633119a2224114f67f63a4559"},
+ {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a76ba5fc8dd9c913640292df27bff80a685bed3a3c990d59aa6ce24c352f8fc"},
+ {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ff70ef093895fd53f4055ca75f93f047e088d1430888ca1229393a7c0521100f"},
+ {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f4244b7018b5753ecd10a6d324ec1f347da130c953a9c88432c7fbc8875d13be"},
+ {file = "orjson-3.10.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16135ccca03445f37921fa4b585cff9a58aa8d81ebcb27622e69bfadd220b32c"},
+ {file = "orjson-3.10.12-cp312-none-win32.whl", hash = "sha256:2d879c81172d583e34153d524fcba5d4adafbab8349a7b9f16ae511c2cee8708"},
+ {file = "orjson-3.10.12-cp312-none-win_amd64.whl", hash = "sha256:fc23f691fa0f5c140576b8c365bc942d577d861a9ee1142e4db468e4e17094fb"},
+ {file = "orjson-3.10.12-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47962841b2a8aa9a258b377f5188db31ba49af47d4003a32f55d6f8b19006543"},
+ {file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6334730e2532e77b6054e87ca84f3072bee308a45a452ea0bffbbbc40a67e296"},
+ {file = "orjson-3.10.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:accfe93f42713c899fdac2747e8d0d5c659592df2792888c6c5f829472e4f85e"},
+ {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7974c490c014c48810d1dede6c754c3cc46598da758c25ca3b4001ac45b703f"},
+ {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3f250ce7727b0b2682f834a3facff88e310f52f07a5dcfd852d99637d386e79e"},
+ {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f31422ff9486ae484f10ffc51b5ab2a60359e92d0716fcce1b3593d7bb8a9af6"},
+ {file = "orjson-3.10.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5f29c5d282bb2d577c2a6bbde88d8fdcc4919c593f806aac50133f01b733846e"},
+ {file = "orjson-3.10.12-cp313-none-win32.whl", hash = "sha256:f45653775f38f63dc0e6cd4f14323984c3149c05d6007b58cb154dd080ddc0dc"},
+ {file = "orjson-3.10.12-cp313-none-win_amd64.whl", hash = "sha256:229994d0c376d5bdc91d92b3c9e6be2f1fbabd4cc1b59daae1443a46ee5e9825"},
+ {file = "orjson-3.10.12-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7d69af5b54617a5fac5c8e5ed0859eb798e2ce8913262eb522590239db6c6763"},
+ {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ed119ea7d2953365724a7059231a44830eb6bbb0cfead33fcbc562f5fd8f935"},
+ {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5fc1238ef197e7cad5c91415f524aaa51e004be5a9b35a1b8a84ade196f73f"},
+ {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43509843990439b05f848539d6f6198d4ac86ff01dd024b2f9a795c0daeeab60"},
+ {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f72e27a62041cfb37a3de512247ece9f240a561e6c8662276beaf4d53d406db4"},
+ {file = "orjson-3.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a904f9572092bb6742ab7c16c623f0cdccbad9eeb2d14d4aa06284867bddd31"},
+ {file = "orjson-3.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:855c0833999ed5dc62f64552db26f9be767434917d8348d77bacaab84f787d7b"},
+ {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:897830244e2320f6184699f598df7fb9db9f5087d6f3f03666ae89d607e4f8ed"},
+ {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:0b32652eaa4a7539f6f04abc6243619c56f8530c53bf9b023e1269df5f7816dd"},
+ {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:36b4aa31e0f6a1aeeb6f8377769ca5d125db000f05c20e54163aef1d3fe8e833"},
+ {file = "orjson-3.10.12-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5535163054d6cbf2796f93e4f0dbc800f61914c0e3c4ed8499cf6ece22b4a3da"},
+ {file = "orjson-3.10.12-cp38-none-win32.whl", hash = "sha256:90a5551f6f5a5fa07010bf3d0b4ca2de21adafbbc0af6cb700b63cd767266cb9"},
+ {file = "orjson-3.10.12-cp38-none-win_amd64.whl", hash = "sha256:703a2fb35a06cdd45adf5d733cf613cbc0cb3ae57643472b16bc22d325b5fb6c"},
+ {file = "orjson-3.10.12-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f29de3ef71a42a5822765def1febfb36e0859d33abf5c2ad240acad5c6a1b78d"},
+ {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de365a42acc65d74953f05e4772c974dad6c51cfc13c3240899f534d611be967"},
+ {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a5a0158648a67ff0004cb0df5df7dcc55bfc9ca154d9c01597a23ad54c8d0c"},
+ {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c47ce6b8d90fe9646a25b6fb52284a14ff215c9595914af63a5933a49972ce36"},
+ {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0eee4c2c5bfb5c1b47a5db80d2ac7aaa7e938956ae88089f098aff2c0f35d5d8"},
+ {file = "orjson-3.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d3081bbe8b86587eb5c98a73b97f13d8f9fea685cf91a579beddacc0d10566"},
+ {file = "orjson-3.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c23a6e90383884068bc2dba83d5222c9fcc3b99a0ed2411d38150734236755"},
+ {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5472be7dc3269b4b52acba1433dac239215366f89dc1d8d0e64029abac4e714e"},
+ {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7319cda750fca96ae5973efb31b17d97a5c5225ae0bc79bf5bf84df9e1ec2ab6"},
+ {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:74d5ca5a255bf20b8def6a2b96b1e18ad37b4a122d59b154c458ee9494377f80"},
+ {file = "orjson-3.10.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ff31d22ecc5fb85ef62c7d4afe8301d10c558d00dd24274d4bbe464380d3cd69"},
+ {file = "orjson-3.10.12-cp39-none-win32.whl", hash = "sha256:c22c3ea6fba91d84fcb4cda30e64aff548fcf0c44c876e681f47d61d24b12e6b"},
+ {file = "orjson-3.10.12-cp39-none-win_amd64.whl", hash = "sha256:be604f60d45ace6b0b33dd990a66b4526f1a7a186ac411c942674625456ca548"},
+ {file = "orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff"},
]
[[package]]
name = "packaging"
-version = "24.1"
+version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
- {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
- {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
+ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
+ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
[[package]]
@@ -677,13 +727,13 @@ files = [
[[package]]
name = "psycopg-pool"
-version = "3.2.3"
+version = "3.2.4"
description = "Connection Pool for Psycopg"
optional = false
python-versions = ">=3.8"
files = [
- {file = "psycopg_pool-3.2.3-py3-none-any.whl", hash = "sha256:53bd8e640625e01b2927b2ad96df8ed8e8f91caea4597d45e7673fc7bbb85eb1"},
- {file = "psycopg_pool-3.2.3.tar.gz", hash = "sha256:bb942f123bef4b7fbe4d55421bd3fb01829903c95c0f33fd42b7e94e5ac9b52a"},
+ {file = "psycopg_pool-3.2.4-py3-none-any.whl", hash = "sha256:f6a22cff0f21f06d72fb2f5cb48c618946777c49385358e0c88d062c59cbd224"},
+ {file = "psycopg_pool-3.2.4.tar.gz", hash = "sha256:61774b5bbf23e8d22bedc7504707135aaf744679f8ef9b3fe29942920746a6ed"},
]
[package.dependencies]
@@ -691,122 +741,131 @@ typing-extensions = ">=4.6"
[[package]]
name = "pydantic"
-version = "2.8.2"
+version = "2.10.2"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
- {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
+ {file = "pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e"},
+ {file = "pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa"},
]
[package.dependencies]
-annotated-types = ">=0.4.0"
-pydantic-core = "2.20.1"
-typing-extensions = [
- {version = ">=4.6.1", markers = "python_version < \"3.13\""},
- {version = ">=4.12.2", markers = "python_version >= \"3.13\""},
-]
+annotated-types = ">=0.6.0"
+pydantic-core = "2.27.1"
+typing-extensions = ">=4.12.2"
[package.extras]
email = ["email-validator (>=2.0.0)"]
+timezone = ["tzdata"]
[[package]]
name = "pydantic-core"
-version = "2.20.1"
+version = "2.27.1"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"},
- {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"},
- {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"},
- {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"},
- {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"},
- {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"},
- {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"},
- {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"},
- {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"},
- {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"},
- {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"},
- {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"},
- {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"},
- {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"},
- {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"},
- {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"},
- {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"},
- {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"},
- {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"},
- {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"},
- {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"},
- {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"},
- {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"},
- {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"},
- {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"},
- {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"},
- {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"},
- {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"},
- {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"},
- {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"},
- {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"},
- {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"},
- {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"},
- {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"},
- {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"},
- {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"},
- {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"},
- {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"},
- {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"},
- {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"},
- {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"},
- {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"},
- {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"},
- {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"},
- {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"},
- {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"},
- {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"},
- {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"},
- {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"},
- {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"},
- {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"},
- {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"},
- {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"},
- {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"},
- {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"},
- {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"},
- {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"},
- {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"},
- {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"},
- {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"},
- {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"},
- {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"},
- {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"},
- {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"},
- {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"},
- {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"},
- {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"},
- {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"},
- {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"},
- {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"},
- {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"},
- {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"},
- {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"},
- {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"},
- {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"},
- {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"},
- {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"},
- {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"},
- {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"},
- {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"},
- {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"},
- {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"},
- {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"},
- {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"},
- {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"},
- {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"},
- {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"},
- {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"},
- {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"},
+ {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"},
+ {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"},
+ {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"},
+ {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"},
+ {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"},
+ {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"},
+ {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"},
+ {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"},
+ {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"},
+ {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"},
+ {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"},
+ {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"},
+ {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"},
+ {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"},
+ {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"},
+ {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"},
+ {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"},
+ {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"},
+ {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"},
+ {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"},
+ {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"},
+ {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"},
+ {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"},
+ {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"},
]
[package.dependencies]
@@ -887,62 +946,64 @@ watchdog = ">=0.6.0"
[[package]]
name = "pyyaml"
-version = "6.0.1"
+version = "6.0.2"
description = "YAML parser and emitter for Python"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"
files = [
- {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
- {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
- {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
- {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
- {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
- {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
- {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
- {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
- {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
- {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
- {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
- {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
- {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
- {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
- {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
- {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
- {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
- {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
- {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
- {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
- {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
- {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
- {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
- {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
- {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
- {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
- {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
- {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
- {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
- {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
- {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
- {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
- {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
- {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
- {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
- {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
- {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
- {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
- {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
- {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
- {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
- {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
- {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
- {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
- {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
- {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
- {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
- {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
- {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
- {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
- {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
+ {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
+ {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"},
+ {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"},
+ {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"},
+ {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"},
+ {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"},
+ {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"},
+ {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"},
+ {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"},
+ {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"},
+ {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"},
+ {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"},
+ {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"},
+ {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"},
+ {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"},
+ {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"},
+ {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"},
+ {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"},
+ {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"},
+ {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"},
+ {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"},
+ {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"},
+ {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"},
+ {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"},
+ {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"},
+ {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"},
+ {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"},
+ {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"},
+ {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"},
+ {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"},
+ {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"},
+ {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"},
+ {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"},
+ {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"},
+ {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"},
+ {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"},
+ {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"},
+ {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"},
+ {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"},
+ {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"},
+ {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"},
+ {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"},
+ {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"},
+ {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"},
+ {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"},
+ {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"},
+ {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"},
+ {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"},
+ {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"},
+ {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"},
+ {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"},
+ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
]
[[package]]
@@ -966,31 +1027,45 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+[[package]]
+name = "requests-toolbelt"
+version = "1.0.0"
+description = "A utility belt for advanced users of python-requests"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
+ {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
+]
+
+[package.dependencies]
+requests = ">=2.0.1,<3.0.0"
+
[[package]]
name = "ruff"
-version = "0.6.2"
+version = "0.6.9"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
- {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"},
- {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"},
- {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"},
- {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"},
- {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"},
- {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"},
- {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"},
- {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"},
- {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"},
- {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"},
- {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"},
+ {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"},
+ {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"},
+ {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"},
+ {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"},
+ {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"},
+ {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"},
+ {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"},
+ {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"},
+ {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"},
+ {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"},
+ {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"},
+ {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"},
+ {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"},
+ {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"},
+ {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"},
+ {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"},
+ {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"},
+ {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"},
]
[[package]]
@@ -1006,13 +1081,13 @@ files = [
[[package]]
name = "tenacity"
-version = "8.5.0"
+version = "9.0.0"
description = "Retry code until it succeeds"
optional = false
python-versions = ">=3.8"
files = [
- {file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"},
- {file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"},
+ {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"},
+ {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"},
]
[package.extras]
@@ -1021,13 +1096,43 @@ test = ["pytest", "tornado (>=4.5)", "typeguard"]
[[package]]
name = "tomli"
-version = "2.0.1"
+version = "2.2.1"
description = "A lil' TOML parser"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
- {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
+ {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
+ {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
+ {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
+ {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
+ {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
+ {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
+ {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
+ {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
+ {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
+ {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
+ {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
+ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
+ {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
]
[[package]]
@@ -1043,24 +1148,24 @@ files = [
[[package]]
name = "tzdata"
-version = "2024.1"
+version = "2024.2"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
files = [
- {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
- {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
+ {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"},
+ {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"},
]
[[package]]
name = "urllib3"
-version = "2.2.2"
+version = "2.2.3"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
files = [
- {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
- {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
+ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"},
+ {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"},
]
[package.extras]
@@ -1071,43 +1176,41 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "watchdog"
-version = "4.0.1"
+version = "6.0.0"
description = "Filesystem events monitoring"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
files = [
- {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"},
- {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"},
- {file = "watchdog-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b"},
- {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5"},
- {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767"},
- {file = "watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459"},
- {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175"},
- {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7"},
- {file = "watchdog-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28"},
- {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35"},
- {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db"},
- {file = "watchdog-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709"},
- {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba"},
- {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235"},
- {file = "watchdog-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682"},
- {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7"},
- {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5"},
- {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193"},
- {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625"},
- {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd"},
- {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee"},
- {file = "watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253"},
- {file = "watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d"},
- {file = "watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6"},
- {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57"},
- {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e"},
- {file = "watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5"},
- {file = "watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84"},
- {file = "watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429"},
- {file = "watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a"},
- {file = "watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d"},
- {file = "watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44"},
+ {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"},
+ {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"},
+ {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"},
+ {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"},
+ {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"},
+ {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"},
+ {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"},
+ {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"},
+ {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"},
+ {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"},
+ {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"},
+ {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"},
+ {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"},
+ {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"},
+ {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"},
+ {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"},
+ {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"},
+ {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"},
+ {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"},
+ {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"},
+ {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"},
+ {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"},
+ {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"},
+ {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"},
]
[package.extras]
@@ -1116,4 +1219,4 @@ watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0,<4.0"
-content-hash = "6bd85ce8ee1192995c1ff03d5fa65af8ee7872214d71b84559a6192cadf82be6"
+content-hash = "35bd5ff50127337dbbdd1b9937bae6190d8f5c9a6808e334c7f45e5a471773fe"
diff --git a/libs/checkpoint-postgres/pyproject.toml b/libs/checkpoint-postgres/pyproject.toml
index bfefeba1d..c3602743b 100644
--- a/libs/checkpoint-postgres/pyproject.toml
+++ b/libs/checkpoint-postgres/pyproject.toml
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
-langgraph-checkpoint = "^2.0.2"
+langgraph-checkpoint = "^2.0.7"
orjson = ">=3.10.1"
psycopg = "^3.0.0"
psycopg-pool = "^3.0.0"
From ee8653d1c5e96c170d8028df1d50c55bf916b9a9 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 27 Nov 2024 22:50:52 -0800
Subject: [PATCH 072/149] [SDK] Add SearchItem (#2567)
---
libs/sdk-py/langgraph_sdk/client.py | 32 +++++++++++++++++++++--------
libs/sdk-py/langgraph_sdk/schema.py | 13 +++++++++++-
libs/sdk-py/pyproject.toml | 2 +-
3 files changed, 37 insertions(+), 10 deletions(-)
diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py
index 6a3bb6c9c..81e8a8506 100644
--- a/libs/sdk-py/langgraph_sdk/client.py
+++ b/libs/sdk-py/langgraph_sdk/client.py
@@ -18,6 +18,7 @@ from typing import (
Dict,
Iterator,
List,
+ Literal,
Optional,
Sequence,
Union,
@@ -1946,7 +1947,7 @@ class CronClient:
Example Usage:
- cron_run = await client.crons.create(
+ cron_run = client.crons.create(
assistant_id="agent",
schedule="27 15 * * *",
input={"messages": [{"role": "user", "content": "hello!"}]},
@@ -2070,7 +2071,12 @@ class StoreClient:
self.http = http
async def put_item(
- self, namespace: Sequence[str], /, key: str, value: dict[str, Any]
+ self,
+ namespace: Sequence[str],
+ /,
+ key: str,
+ value: dict[str, Any],
+ index: Optional[Union[Literal[False], list[str]]] = None,
) -> None:
"""Store or update an item.
@@ -2078,6 +2084,7 @@ class StoreClient:
namespace: A list of strings representing the namespace path.
key: The unique identifier for the item within the namespace.
value: A dictionary containing the item's data.
+ index: Controls search indexing - None (use defaults), False (disable), or list of field paths to index.
Returns:
None
@@ -2095,11 +2102,7 @@ class StoreClient:
raise ValueError(
f"Invalid namespace label '{label}'. Namespace labels cannot contain periods ('.')."
)
- payload = {
- "namespace": namespace,
- "key": key,
- "value": value,
- }
+ payload = {"namespace": namespace, "key": key, "value": value, "index": index}
await self.http.put("/store/items", json=payload)
async def get_item(self, namespace: Sequence[str], /, key: str) -> Item:
@@ -2167,6 +2170,7 @@ class StoreClient:
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
+ query: Optional[str] = None,
) -> SearchItemsResponse:
"""Search for items within a namespace prefix.
@@ -2175,6 +2179,7 @@ class StoreClient:
filter: Optional dictionary of key-value pairs to filter results.
limit: Maximum number of items to return (default is 10).
offset: Number of items to skip before returning results (default is 0).
+ query: Optional query for natural language search.
Returns:
List[Item]: A list of items matching the search criteria.
@@ -2212,6 +2217,7 @@ class StoreClient:
"filter": filter,
"limit": limit,
"offset": offset,
+ "query": query,
}
return await self.http.post("/store/items/search", json=_provided_vals(payload))
@@ -4154,7 +4160,12 @@ class SyncStoreClient:
self.http = http
def put_item(
- self, namespace: Sequence[str], /, key: str, value: dict[str, Any]
+ self,
+ namespace: Sequence[str],
+ /,
+ key: str,
+ value: dict[str, Any],
+ index: Optional[Union[Literal[False], list[str]]] = None,
) -> None:
"""Store or update an item.
@@ -4162,6 +4173,7 @@ class SyncStoreClient:
namespace: A list of strings representing the namespace path.
key: The unique identifier for the item within the namespace.
value: A dictionary containing the item's data.
+ index: Controls search indexing - None (use defaults), False (disable), or list of field paths to index.
Returns:
None
@@ -4183,6 +4195,7 @@ class SyncStoreClient:
"namespace": namespace,
"key": key,
"value": value,
+ "index": index,
}
self.http.put("/store/items", json=payload)
@@ -4250,6 +4263,7 @@ class SyncStoreClient:
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
+ query: Optional[str] = None,
) -> SearchItemsResponse:
"""Search for items within a namespace prefix.
@@ -4258,6 +4272,7 @@ class SyncStoreClient:
filter: Optional dictionary of key-value pairs to filter results.
limit: Maximum number of items to return (default is 10).
offset: Number of items to skip before returning results (default is 0).
+ query: Optional query for natural language search.
Returns:
List[Item]: A list of items matching the search criteria.
@@ -4295,6 +4310,7 @@ class SyncStoreClient:
"filter": filter,
"limit": limit,
"offset": offset,
+ "query": query,
}
return self.http.post("/store/items/search", json=_provided_vals(payload))
diff --git a/libs/sdk-py/langgraph_sdk/schema.py b/libs/sdk-py/langgraph_sdk/schema.py
index 5264ce709..a66d007f5 100644
--- a/libs/sdk-py/langgraph_sdk/schema.py
+++ b/libs/sdk-py/langgraph_sdk/schema.py
@@ -325,10 +325,21 @@ class ListNamespaceResponse(TypedDict):
"""A list of namespace paths, where each path is a list of strings."""
+class SearchItem(Item, total=False):
+ """Item with an optional relevance score from search operations.
+
+ Attributes:
+ score (Optional[float]): Relevance/similarity score. Included when
+ searching a compatible store with a natural language query.
+ """
+
+ score: Optional[float]
+
+
class SearchItemsResponse(TypedDict):
"""Response structure for searching items."""
- items: list[Item]
+ items: list[SearchItem]
"""A list of items matching the search criteria."""
diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml
index 7c8af51b2..c7776a6cc 100644
--- a/libs/sdk-py/pyproject.toml
+++ b/libs/sdk-py/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
-version = "0.1.39"
+version = "0.1.40"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"
From 1130c3accb4c50fa539f4b26130513f2d4d506a9 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Thu, 28 Nov 2024 01:58:18 -0800
Subject: [PATCH 073/149] [CLI] Add Store config to CLI (#2548)
---
libs/cli/langgraph_cli/cli.py | 31 ++++++----
libs/cli/langgraph_cli/config.py | 68 +++++++++++++++++++--
libs/cli/poetry.lock | 75 +++++++++++++++++-------
libs/cli/pyproject.toml | 4 +-
libs/cli/tests/unit_tests/test_config.py | 2 +
5 files changed, 139 insertions(+), 41 deletions(-)
diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py
index c304252b3..6c85f6b06 100644
--- a/libs/cli/langgraph_cli/cli.py
+++ b/libs/cli/langgraph_cli/cli.py
@@ -511,19 +511,6 @@ def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -
)
-@click.argument("path", required=False)
-@click.option(
- "--template",
- type=str,
- help=TEMPLATE_HELP_STRING,
-)
-@cli.command("new", help="🌱 Create a new LangGraph project from a template.")
-@log_command
-def new(path: Optional[str], template: Optional[str]) -> None:
- """Create a new LangGraph project from a template."""
- return create_new(path, template)
-
-
@click.option(
"--host",
default="127.0.0.1",
@@ -608,6 +595,10 @@ def dev(
sys.path.append(str(dep_path))
graphs = config_json.get("graphs", {})
+ additional_config = {}
+ if config_json.get("store"):
+ additional_config["store"] = config_json["store"]
+
run_server(
host,
port,
@@ -617,9 +608,23 @@ def dev(
open_browser=not no_browser,
debug_port=debug_port,
env=config_json.get("env", None),
+ config=additional_config,
)
+@click.argument("path", required=False)
+@click.option(
+ "--template",
+ type=str,
+ help=TEMPLATE_HELP_STRING,
+)
+@cli.command("new", help="🌱 Create a new LangGraph project from a template.")
+@log_command
+def new(path: Optional[str], template: Optional[str]) -> None:
+ """Create a new LangGraph project from a template."""
+ return create_new(path, template)
+
+
def prepare_args_and_stdin(
*,
capabilities: DockerCapabilities,
diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py
index 95b72e0cd..aa6e903a3 100644
--- a/libs/cli/langgraph_cli/config.py
+++ b/libs/cli/langgraph_cli/config.py
@@ -10,7 +10,44 @@ MIN_NODE_VERSION = "20"
MIN_PYTHON_VERSION = "3.11"
-class Config(TypedDict):
+class IndexConfig(TypedDict, total=False):
+ """Configuration for indexing documents for semantic search in the store."""
+
+ dims: int
+ """Number of dimensions in the embedding vectors.
+
+ Common embedding models have the following dimensions:
+ - OpenAI text-embedding-3-large: 256, 1024, or 3072
+ - OpenAI text-embedding-3-small: 512 or 1536
+ - OpenAI text-embedding-ada-002: 1536
+ - Cohere embed-english-v3.0: 1024
+ - Cohere embed-english-light-v3.0: 384
+ - Cohere embed-multilingual-v3.0: 1024
+ - Cohere embed-multilingual-light-v3.0: 384
+ """
+
+ embed: str
+ """Optional model (string) to generate embeddings from text or path to model or function.
+
+ Examples:
+ - "openai:text-embedding-3-large"
+ - "cohere:embed-multilingual-v3.0"
+ - "src/app.py:embeddings
+ """
+
+ fields: Optional[list[str]]
+ """Fields to extract text from for embedding generation.
+
+ Defaults to the root ["$"], which embeds the json object as a whole.
+ """
+
+
+class StoreConfig(TypedDict, total=False):
+ embed: Optional[IndexConfig]
+ """Configuration for vector embeddings in store."""
+
+
+class Config(TypedDict, total=False):
python_version: str
node_version: Optional[str]
pip_config_file: Optional[str]
@@ -18,6 +55,7 @@ class Config(TypedDict):
dependencies: list[str]
graphs: dict[str, str]
env: Union[dict[str, str], str]
+ store: Optional[StoreConfig]
def _parse_version(version_str: str) -> tuple[int, int]:
@@ -49,6 +87,7 @@ def validate_config(config: Config) -> Config:
"dockerfile_lines": config.get("dockerfile_lines", []),
"graphs": config.get("graphs", {}),
"env": config.get("env", {}),
+ "store": config.get("store"),
}
if config.get("node_version")
else {
@@ -58,6 +97,7 @@ def validate_config(config: Config) -> Config:
"dependencies": config.get("dependencies", []),
"graphs": config.get("graphs", {}),
"env": config.get("env", {}),
+ "store": config.get("store"),
}
)
@@ -352,7 +392,16 @@ RUN set -ex && \\
],
)
)
-
+ additional_config = {}
+ if config.get("store"):
+ additional_config["store"] = config["store"]
+ env_additional_config = (
+ ""
+ if not additional_config
+ else f"""
+ENV LANGGRAPH_CONFIG='{json.dumps(additional_config)}'
+"""
+ )
return f"""FROM {base_image}:{config['python_version']}
{os.linesep.join(config["dockerfile_lines"])}
@@ -360,7 +409,7 @@ RUN set -ex && \\
{installs}
RUN {pip_install} -e /deps/*
-
+{env_additional_config}
ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
{f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else ""}"""
@@ -390,7 +439,16 @@ def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image:
install_cmd = "npm ci"
else:
install_cmd = "npm i"
-
+ additional_config = {}
+ if config.get("store"):
+ additional_config["store"] = config["store"]
+ env_additional_config = (
+ ""
+ if not additional_config
+ else f"""
+ENV LANGGRAPH_CONFIG='{json.dumps(additional_config)}'
+"""
+ )
return f"""FROM {base_image}:{config['node_version']}
{os.linesep.join(config["dockerfile_lines"])}
@@ -398,7 +456,7 @@ def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image:
ADD . {faux_path}
RUN cd {faux_path} && {install_cmd}
-
+{env_additional_config}
ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
WORKDIR {faux_path}
diff --git a/libs/cli/poetry.lock b/libs/cli/poetry.lock
index 29045a438..9761e08a4 100644
--- a/libs/cli/poetry.lock
+++ b/libs/cli/poetry.lock
@@ -565,13 +565,13 @@ langgraph-sdk = ">=0.1.32,<0.2.0"
[[package]]
name = "langgraph-api"
-version = "0.0.2"
+version = "0.0.5"
description = ""
optional = true
python-versions = "<4.0,>=3.11.0"
files = [
- {file = "langgraph_api-0.0.2-py3-none-any.whl", hash = "sha256:7a30fb21987572eacc93dd1c69c2155c17957afed71dde18d6f47992b3124d65"},
- {file = "langgraph_api-0.0.2.tar.gz", hash = "sha256:b751afca96cb6db67fe2f48e798ada27a4df068f0df86b36d2b8eee52344bbf0"},
+ {file = "langgraph_api-0.0.5-py3-none-any.whl", hash = "sha256:9c981c489924f5d7e67ce7a3a9908ede15ac266e699deff6f5de691af5b49931"},
+ {file = "langgraph_api-0.0.5.tar.gz", hash = "sha256:f7ff041f1706152a2587916f0373f513e456cde067c94f8266ff29f6af908d20"},
]
[package.dependencies]
@@ -579,8 +579,8 @@ cryptography = ">=43.0.3,<44.0.0"
httpx = ">=0.27.0"
jsonschema-rs = ">=0.25.0,<0.26.0"
langchain-core = ">=0.2.38,<0.4.0"
-langgraph = ">=0.2.52"
-langgraph-checkpoint = ">=2.0.5,<3.0"
+langgraph = ">=0.2.52,<0.3.0"
+langgraph-checkpoint = ">=2.0.7,<3.0"
langsmith = ">=0.1.63,<0.2.0"
orjson = ">=3.10.1"
pyjwt = ">=2.9.0,<3.0.0"
@@ -593,13 +593,13 @@ watchfiles = ">=0.13"
[[package]]
name = "langgraph-checkpoint"
-version = "2.0.6"
+version = "2.0.7"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = true
python-versions = "<4.0.0,>=3.9.0"
files = [
- {file = "langgraph_checkpoint-2.0.6-py3-none-any.whl", hash = "sha256:2878283c3ee2519bf180df9b7b7155b73fa05eb63b1af9600a03e03a930d8c53"},
- {file = "langgraph_checkpoint-2.0.6.tar.gz", hash = "sha256:69ab9c61c4e2992264671f55579c24070b7b6cedc105a33da3fba6526df248cf"},
+ {file = "langgraph_checkpoint-2.0.7-py3-none-any.whl", hash = "sha256:9709f672e1c5a47e13352067c2ffa114dd91d443967b7ce8a1d36d6fc170370e"},
+ {file = "langgraph_checkpoint-2.0.7.tar.gz", hash = "sha256:88d648a331d20aa8ce65280de34a34a9190380b004f6afcc5f9894fe3abeed08"},
]
[package.dependencies]
@@ -608,13 +608,13 @@ msgpack = ">=1.1.0,<2.0.0"
[[package]]
name = "langgraph-sdk"
-version = "0.1.36"
+version = "0.1.40"
description = "SDK for interacting with LangGraph API"
optional = true
python-versions = "<4.0.0,>=3.9.0"
files = [
- {file = "langgraph_sdk-0.1.36-py3-none-any.whl", hash = "sha256:b11e1f0bc67631134d09d50c812dc73f9eb30394764ae1144d7d2a786a715355"},
- {file = "langgraph_sdk-0.1.36.tar.gz", hash = "sha256:2a2c651b7851ba15aeaab7e4e3ea7fd8357ef1cb0b592f264916fa990cdda6e7"},
+ {file = "langgraph_sdk-0.1.40-py3-none-any.whl", hash = "sha256:8810cca5e4144cf3a5441fc76b4ee6e658ec95f932d3a0bf9ad63de117e925b9"},
+ {file = "langgraph_sdk-0.1.40.tar.gz", hash = "sha256:ab2719ac7274612a791a7a0ad9395d250357106cba8ba81bca9968fc91009af2"},
]
[package.dependencies]
@@ -624,13 +624,13 @@ orjson = ">=3.10.1"
[[package]]
name = "langsmith"
-version = "0.1.146"
+version = "0.1.147"
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
optional = true
python-versions = "<4.0,>=3.8.1"
files = [
- {file = "langsmith-0.1.146-py3-none-any.whl", hash = "sha256:9d062222f1a32c9b047dab0149b24958f988989cd8d4a5f9139ff959a51e59d8"},
- {file = "langsmith-0.1.146.tar.gz", hash = "sha256:ead8b0b9d5b6cd3ac42937ec48bdf09d4afe7ca1bba22dc05eb65591a18106f8"},
+ {file = "langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15"},
+ {file = "langsmith-0.1.147.tar.gz", hash = "sha256:2e933220318a4e73034657103b3b1a3a6109cc5db3566a7e8e03be8d6d7def7a"},
]
[package.dependencies]
@@ -643,6 +643,9 @@ pydantic = [
requests = ">=2,<3"
requests-toolbelt = ">=1.0.0,<2.0.0"
+[package.extras]
+langsmith-pyo3 = ["langsmith-pyo3 (>=0.1.0rc2,<0.2.0)"]
+
[[package]]
name = "msgpack"
version = "1.1.0"
@@ -1035,13 +1038,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pyjwt"
-version = "2.10.0"
+version = "2.10.1"
description = "JSON Web Token implementation in Python"
optional = true
python-versions = ">=3.9"
files = [
- {file = "PyJWT-2.10.0-py3-none-any.whl", hash = "sha256:543b77207db656de204372350926bed5a86201c4cbff159f623f79c7bb487a15"},
- {file = "pyjwt-2.10.0.tar.gz", hash = "sha256:7628a7eb7938959ac1b26e819a1df0fd3259505627b575e4bad6d08f76db695c"},
+ {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"},
+ {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"},
]
[package.extras]
@@ -1342,13 +1345,43 @@ test = ["pytest", "tornado (>=4.5)", "typeguard"]
[[package]]
name = "tomli"
-version = "2.1.0"
+version = "2.2.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
files = [
- {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"},
- {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"},
+ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
+ {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
+ {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
+ {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
+ {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
+ {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
+ {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
+ {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
+ {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
+ {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
+ {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
+ {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
+ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
+ {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
]
[[package]]
@@ -1528,4 +1561,4 @@ inmem = ["langgraph-api", "python-dotenv"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0,<4.0"
-content-hash = "3d655bb578e20219e19152d4a3d86be370fe3be61b5559847f0204dfff499b4a"
+content-hash = "7388b141c48dd6cfa33b504822e5348ac514ac2e2711d37368d7cb7103c8340a"
diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml
index fcb3d1979..f2987d2f8 100644
--- a/libs/cli/pyproject.toml
+++ b/libs/cli/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
-version = "0.1.59"
+version = "0.1.60"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
@@ -14,7 +14,7 @@ langgraph = "langgraph_cli.cli:cli"
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
click = "^8.1.7"
-langgraph-api = { version = ">=0.0.2,<0.1.0", optional = true, python = ">=3.11,<4.0" }
+langgraph-api = { version = ">=0.0.5,<0.1.0", optional = true, python = ">=3.11,<4.0" }
python-dotenv = { version = ">=0.8.0", optional = true }
[tool.poetry.group.dev.dependencies]
diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py
index c1632a8b3..6da247077 100644
--- a/libs/cli/tests/unit_tests/test_config.py
+++ b/libs/cli/tests/unit_tests/test_config.py
@@ -30,6 +30,7 @@ def test_validate_config():
"pip_config_file": None,
"dockerfile_lines": [],
"env": {},
+ "store": None,
**expected_config,
}
actual_config = validate_config(expected_config)
@@ -46,6 +47,7 @@ def test_validate_config():
"agent": "./agent.py:graph",
},
"env": env,
+ "store": None,
}
actual_config = validate_config(expected_config)
assert actual_config == expected_config
From 65172c2a432dc9eaead0caec568350732ddd669e Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Thu, 28 Nov 2024 12:24:00 -0800
Subject: [PATCH 074/149] [CLI] Nonblocking debugpy mode (#2573)
---
libs/cli/langgraph_cli/cli.py | 15 ++++++++++-----
libs/cli/langgraph_cli/config.py | 16 ++++++----------
libs/cli/poetry.lock | 8 ++++----
libs/cli/pyproject.toml | 4 ++--
4 files changed, 22 insertions(+), 21 deletions(-)
diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py
index 6c85f6b06..d651529a3 100644
--- a/libs/cli/langgraph_cli/cli.py
+++ b/libs/cli/langgraph_cli/cli.py
@@ -550,6 +550,12 @@ def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -
type=int,
help="Enable remote debugging by listening on specified port. Requires debugpy to be installed",
)
+@click.option(
+ "--wait-for-client",
+ is_flag=True,
+ help="Wait for a debugger client to connect to the debug port before starting the server",
+ default=False,
+)
@cli.command(
"dev",
help="🏃♀️➡️ Run LangGraph API server in development mode with hot reloading and debugging support",
@@ -563,6 +569,7 @@ def dev(
n_jobs_per_worker: Optional[int],
no_browser: bool,
debug_port: Optional[int],
+ wait_for_client: bool,
):
"""CLI entrypoint for running the LangGraph API server."""
try:
@@ -595,9 +602,6 @@ def dev(
sys.path.append(str(dep_path))
graphs = config_json.get("graphs", {})
- additional_config = {}
- if config_json.get("store"):
- additional_config["store"] = config_json["store"]
run_server(
host,
@@ -607,8 +611,9 @@ def dev(
n_jobs_per_worker=n_jobs_per_worker,
open_browser=not no_browser,
debug_port=debug_port,
- env=config_json.get("env", None),
- config=additional_config,
+ env=config_json.get("env"),
+ store=config_json.get("store"),
+ wait_for_client=wait_for_client,
)
diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py
index aa6e903a3..99db440e1 100644
--- a/libs/cli/langgraph_cli/config.py
+++ b/libs/cli/langgraph_cli/config.py
@@ -392,14 +392,12 @@ RUN set -ex && \\
],
)
)
- additional_config = {}
- if config.get("store"):
- additional_config["store"] = config["store"]
+ store_config = config.get("store")
env_additional_config = (
""
- if not additional_config
+ if not store_config
else f"""
-ENV LANGGRAPH_CONFIG='{json.dumps(additional_config)}'
+ENV LANGGRAPH_STORE='{json.dumps(store_config)}'
"""
)
return f"""FROM {base_image}:{config['python_version']}
@@ -439,14 +437,12 @@ def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image:
install_cmd = "npm ci"
else:
install_cmd = "npm i"
- additional_config = {}
- if config.get("store"):
- additional_config["store"] = config["store"]
+ store_config = config.get("store")
env_additional_config = (
""
- if not additional_config
+ if not store_config
else f"""
-ENV LANGGRAPH_CONFIG='{json.dumps(additional_config)}'
+ENV LANGGRAPH_STORE='{json.dumps(store_config)}'
"""
)
return f"""FROM {base_image}:{config['node_version']}
diff --git a/libs/cli/poetry.lock b/libs/cli/poetry.lock
index 9761e08a4..b59bde1b3 100644
--- a/libs/cli/poetry.lock
+++ b/libs/cli/poetry.lock
@@ -565,13 +565,13 @@ langgraph-sdk = ">=0.1.32,<0.2.0"
[[package]]
name = "langgraph-api"
-version = "0.0.5"
+version = "0.0.6"
description = ""
optional = true
python-versions = "<4.0,>=3.11.0"
files = [
- {file = "langgraph_api-0.0.5-py3-none-any.whl", hash = "sha256:9c981c489924f5d7e67ce7a3a9908ede15ac266e699deff6f5de691af5b49931"},
- {file = "langgraph_api-0.0.5.tar.gz", hash = "sha256:f7ff041f1706152a2587916f0373f513e456cde067c94f8266ff29f6af908d20"},
+ {file = "langgraph_api-0.0.6-py3-none-any.whl", hash = "sha256:f64b13959d721143f6a023af5b9ffc9aa054064af98d21d5d8090cda7e7bffd2"},
+ {file = "langgraph_api-0.0.6.tar.gz", hash = "sha256:badac44fa1ec979509e56fc0da57eeb5f278ee5871f27803f73ea6d8822c21b9"},
]
[package.dependencies]
@@ -1561,4 +1561,4 @@ inmem = ["langgraph-api", "python-dotenv"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0,<4.0"
-content-hash = "7388b141c48dd6cfa33b504822e5348ac514ac2e2711d37368d7cb7103c8340a"
+content-hash = "8eaaa66d9e6e447699e3bcee336dfe779b58c956f8c2ad6678008a07be935838"
diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml
index f2987d2f8..24041c244 100644
--- a/libs/cli/pyproject.toml
+++ b/libs/cli/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
-version = "0.1.60"
+version = "0.1.61"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
@@ -14,7 +14,7 @@ langgraph = "langgraph_cli.cli:cli"
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
click = "^8.1.7"
-langgraph-api = { version = ">=0.0.5,<0.1.0", optional = true, python = ">=3.11,<4.0" }
+langgraph-api = { version = ">=0.0.6,<0.1.0", optional = true, python = ">=3.11,<4.0" }
python-dotenv = { version = ">=0.8.0", optional = true }
[tool.poetry.group.dev.dependencies]
From 784821705b5134e36c3e346854b6ae33f22cc100 Mon Sep 17 00:00:00 2001
From: Vadym Barda
Date: Fri, 29 Nov 2024 11:30:19 -0500
Subject: [PATCH 075/149] checkpoint-postgres: pin psycopg >= 3.2.0 (#2580)
---
libs/checkpoint-postgres/poetry.lock | 2 +-
libs/checkpoint-postgres/pyproject.toml | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/libs/checkpoint-postgres/poetry.lock b/libs/checkpoint-postgres/poetry.lock
index a3c23df97..d1cc60d52 100644
--- a/libs/checkpoint-postgres/poetry.lock
+++ b/libs/checkpoint-postgres/poetry.lock
@@ -1219,4 +1219,4 @@ watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0,<4.0"
-content-hash = "35bd5ff50127337dbbdd1b9937bae6190d8f5c9a6808e334c7f45e5a471773fe"
+content-hash = "d64fe96797a79103d952c13d4dc0a0296c468b6615b4872aa01cb34479ca4104"
diff --git a/libs/checkpoint-postgres/pyproject.toml b/libs/checkpoint-postgres/pyproject.toml
index c3602743b..a121a1b39 100644
--- a/libs/checkpoint-postgres/pyproject.toml
+++ b/libs/checkpoint-postgres/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-postgres"
-version = "2.0.5"
+version = "2.0.6"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
@@ -12,8 +12,8 @@ packages = [{ include = "langgraph" }]
python = "^3.9.0,<4.0"
langgraph-checkpoint = "^2.0.7"
orjson = ">=3.10.1"
-psycopg = "^3.0.0"
-psycopg-pool = "^3.0.0"
+psycopg = "^3.2.0"
+psycopg-pool = "^3.2.0"
[tool.poetry.group.dev.dependencies]
ruff = "^0.6.2"
From 363c6e2e4cb3fa4ecf2dc5a3ca606eab04d81311 Mon Sep 17 00:00:00 2001
From: stneng
Date: Sun, 1 Dec 2024 16:07:15 -0800
Subject: [PATCH 076/149] fix
---
libs/langgraph/langgraph/graph/state.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py
index 0684fb29c..508f4e474 100644
--- a/libs/langgraph/langgraph/graph/state.py
+++ b/libs/langgraph/langgraph/graph/state.py
@@ -933,12 +933,12 @@ def _is_field_binop(typ: Type[Any]) -> Optional[BinaryOperatorAggregate]:
if hasattr(typ, "__metadata__"):
meta = typ.__metadata__
if len(meta) >= 1 and callable(meta[-1]):
- sig = signature(meta[0])
+ sig = signature(meta[-1])
params = list(sig.parameters.values())
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])
+ return BinaryOperatorAggregate(typ, meta[-1])
else:
raise ValueError(
f"Invalid reducer signature. Expected (a, b) -> c. Got {sig}"
From 46dd424a7eec665df12c9d0a35741f6b3bf57076 Mon Sep 17 00:00:00 2001
From: Tat Dat Duong
Date: Mon, 2 Dec 2024 19:53:58 +0100
Subject: [PATCH 077/149] feat(sdk): add ability to search runs via status
---
libs/sdk-js/src/client.ts | 7 +++++++
libs/sdk-js/src/schema.ts | 2 +-
libs/sdk-py/langgraph_sdk/client.py | 19 +++++++++++++++----
3 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/libs/sdk-js/src/client.ts b/libs/sdk-js/src/client.ts
index 010864419..ecf7147ca 100644
--- a/libs/sdk-js/src/client.ts
+++ b/libs/sdk-js/src/client.ts
@@ -7,6 +7,7 @@ import {
GraphSchema,
Metadata,
Run,
+ RunStatus,
Thread,
ThreadState,
Cron,
@@ -944,12 +945,18 @@ export class RunsClient extends BaseClient {
* Defaults to 0.
*/
offset?: number;
+
+ /**
+ * Status of the run to filter by.
+ */
+ status?: RunStatus;
},
): Promise {
return this.fetch(`/threads/${threadId}/runs`, {
params: {
limit: options?.limit ?? 10,
offset: options?.offset ?? 0,
+ status: options?.status ?? undefined,
},
});
}
diff --git a/libs/sdk-js/src/schema.ts b/libs/sdk-js/src/schema.ts
index dd79e07ba..f68e3d42f 100644
--- a/libs/sdk-js/src/schema.ts
+++ b/libs/sdk-js/src/schema.ts
@@ -2,7 +2,7 @@ import type { JSONSchema7 } from "json-schema";
type Optional = T | null | undefined;
-type RunStatus =
+export type RunStatus =
| "pending"
| "running"
| "error"
diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py
index 81e8a8506..b7cc1a78c 100644
--- a/libs/sdk-py/langgraph_sdk/client.py
+++ b/libs/sdk-py/langgraph_sdk/client.py
@@ -51,6 +51,7 @@ from langgraph_sdk.schema import (
OnConflictBehavior,
Run,
RunCreate,
+ RunStatus,
SearchItemsResponse,
StreamMode,
StreamPart,
@@ -1684,7 +1685,12 @@ class RunsClient:
return response
async def list(
- self, thread_id: str, *, limit: int = 10, offset: int = 0
+ self,
+ thread_id: str,
+ *,
+ limit: int = 10,
+ offset: int = 0,
+ status: Optional[RunStatus] = None,
) -> List[Run]:
"""List runs.
@@ -1692,6 +1698,7 @@ class RunsClient:
thread_id: The thread ID to list runs for.
limit: The maximum number of results to return.
offset: The number of results to skip.
+ status: The status of the run to filter by.
Returns:
List[Run]: The runs for the thread.
@@ -1705,9 +1712,13 @@ class RunsClient:
)
""" # noqa: E501
- return await self.http.get(
- f"/threads/{thread_id}/runs?limit={limit}&offset={offset}"
- )
+ params = {
+ "limit": limit,
+ "offset": offset,
+ }
+ if status is not None:
+ params["status"] = status
+ return await self.http.get(f"/threads/{thread_id}/runs", params=params)
async def get(self, thread_id: str, run_id: str) -> Run:
"""Get a run.
From 2ce2021c39bf5530c5d329754a3a5cbb329c2fc8 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Mon, 2 Dec 2024 11:29:06 -0800
Subject: [PATCH 078/149] Revert "Revert "sdk-py: Fix SSE parsing to split
lines only \n \r \r\n per SSE spec""
This reverts commit 53ec7c41b2bd4261ba3790f417724a420132d863.
---
libs/sdk-py/langgraph_sdk/client.py | 5 +-
libs/sdk-py/langgraph_sdk/sse.py | 106 ++++++++++++++++++++++++++++
2 files changed, 109 insertions(+), 2 deletions(-)
create mode 100644 libs/sdk-py/langgraph_sdk/sse.py
diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py
index 81e8a8506..ccf652b66 100644
--- a/libs/sdk-py/langgraph_sdk/client.py
+++ b/libs/sdk-py/langgraph_sdk/client.py
@@ -60,6 +60,7 @@ from langgraph_sdk.schema import (
ThreadStatus,
ThreadUpdateStateResponse,
)
+from langgraph_sdk.sse import EventSource
logger = logging.getLogger(__name__)
@@ -293,7 +294,7 @@ class HttpClient:
else:
logger.error(f"Error from langgraph-api: {body}", exc_info=e)
raise e
- async for event in sse.aiter_sse():
+ async for event in EventSource(sse.response).aiter_sse():
yield StreamPart(
event.event, orjson.loads(event.data) if event.data else None
)
@@ -2432,7 +2433,7 @@ class SyncHttpClient:
else:
logger.error(f"Error from langgraph-api: {body}", exc_info=e)
raise e
- for event in sse.iter_sse():
+ for event in EventSource(sse.response).iter_sse():
yield StreamPart(
event.event, orjson.loads(event.data) if event.data else None
)
diff --git a/libs/sdk-py/langgraph_sdk/sse.py b/libs/sdk-py/langgraph_sdk/sse.py
new file mode 100644
index 000000000..8b019e4a6
--- /dev/null
+++ b/libs/sdk-py/langgraph_sdk/sse.py
@@ -0,0 +1,106 @@
+"""Adapted from httpx_sse to split lines on \n, \r, \r\n per the SSE spec."""
+
+import io
+from typing import AsyncIterator, Iterator
+
+import httpx
+import httpx_sse
+import httpx_sse._decoders
+
+
+class BytesLineDecoder:
+ """
+ Handles incrementally reading lines from text.
+
+ Has the same behaviour as the stdllib bytes splitlines,
+ but handling the input iteratively.
+ """
+
+ def __init__(self) -> None:
+ self.buffer = io.BytesIO()
+ self.trailing_cr: bool = False
+
+ def decode(self, text: bytes) -> list[bytes]:
+ # See https://docs.python.org/3/glossary.html#term-universal-newlines
+ NEWLINE_CHARS = b"\n\r"
+
+ # We always push a trailing `\r` into the next decode iteration.
+ if self.trailing_cr:
+ text = b"\r" + text
+ self.trailing_cr = False
+ if text.endswith(b"\r"):
+ self.trailing_cr = True
+ text = text[:-1]
+
+ if not text:
+ # NOTE: the edge case input of empty text doesn't occur in practice,
+ # because other httpx internals filter out this value
+ return [] # pragma: no cover
+
+ trailing_newline = text[-1] in NEWLINE_CHARS
+ lines = text.splitlines()
+
+ if len(lines) == 1 and not trailing_newline:
+ # No new lines, buffer the input and continue.
+ self.buffer.write(lines[0])
+ return []
+
+ if self.buffer:
+ # Include any existing buffer in the first portion of the
+ # splitlines result.
+ lines = [self.buffer.getvalue() + lines[0]] + lines[1:]
+ self.buffer.truncate(0)
+
+ if not trailing_newline:
+ # If the last segment of splitlines is not newline terminated,
+ # then drop it from our output and start a new buffer.
+ self.buffer.write(lines.pop())
+
+ return lines
+
+ def flush(self) -> list[bytes]:
+ if not self.buffer and not self.trailing_cr:
+ return []
+
+ lines = [self.buffer.getvalue()] if self.buffer else []
+ self.buffer.truncate(0)
+ self.trailing_cr = False
+ return lines
+
+
+async def aiter_lines_raw(response: httpx.Response) -> AsyncIterator[bytes]:
+ decoder = BytesLineDecoder()
+ async for chunk in response.aiter_bytes():
+ for line in decoder.decode(chunk):
+ yield line
+ for line in decoder.flush():
+ yield line
+
+
+def iter_lines_raw(response: httpx.Response) -> Iterator[bytes]:
+ decoder = BytesLineDecoder()
+ for chunk in response.iter_bytes():
+ for line in decoder.decode(chunk):
+ yield line
+ for line in decoder.flush():
+ yield line
+
+
+class EventSource(httpx_sse.EventSource):
+ async def aiter_sse(self) -> AsyncIterator[httpx_sse.ServerSentEvent]:
+ self._check_content_type()
+ decoder = httpx_sse._decoders.SSEDecoder()
+ async for line in aiter_lines_raw(self._response):
+ line = line.rstrip(b"\n")
+ sse = decoder.decode(line.decode())
+ if sse is not None:
+ yield sse
+
+ def iter_sse(self) -> Iterator[httpx_sse.ServerSentEvent]:
+ self._check_content_type()
+ decoder = httpx_sse._decoders.SSEDecoder()
+ for line in iter_lines_raw(self._response):
+ line = line.rstrip(b"\n")
+ sse = decoder.decode(line.decode())
+ if sse is not None:
+ yield sse
From 3cee1d5087c92160f0b8acf9929b03ab64e2bb9a Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Mon, 2 Dec 2024 12:03:31 -0800
Subject: [PATCH 079/149] Remove httpx_sse, fix missing flush of sse decoder
---
libs/sdk-py/langgraph_sdk/client.py | 60 ++++++++++-----
libs/sdk-py/langgraph_sdk/sse.py | 112 +++++++++++++++++++---------
libs/sdk-py/poetry.lock | 13 +---
libs/sdk-py/pyproject.toml | 1 -
4 files changed, 120 insertions(+), 66 deletions(-)
diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py
index ccf652b66..3fbf61041 100644
--- a/libs/sdk-py/langgraph_sdk/client.py
+++ b/libs/sdk-py/langgraph_sdk/client.py
@@ -26,7 +26,6 @@ from typing import (
)
import httpx
-import httpx_sse
import orjson
from httpx._types import QueryParamTypes
@@ -60,7 +59,7 @@ from langgraph_sdk.schema import (
ThreadStatus,
ThreadUpdateStateResponse,
)
-from langgraph_sdk.sse import EventSource
+from langgraph_sdk.sse import SSEDecoder, aiter_lines_raw, iter_lines_raw
logger = logging.getLogger(__name__)
@@ -282,22 +281,37 @@ class HttpClient:
) -> AsyncIterator[StreamPart]:
"""Stream results using SSE."""
headers, content = await aencode_json(json)
- async with httpx_sse.aconnect_sse(
- self.client, method, path, headers=headers, content=content
- ) as sse:
+ headers["Accept"] = "text/event-stream"
+ headers["Cache-Control"] = "no-store"
+
+ async with self.client.stream(
+ method, path, headers=headers, content=content
+ ) as res:
+ # check status
try:
- sse.response.raise_for_status()
+ res.raise_for_status()
except httpx.HTTPStatusError as e:
- body = (await sse.response.aread()).decode()
+ body = (await res.aread()).decode()
if sys.version_info >= (3, 11):
e.add_note(body)
else:
logger.error(f"Error from langgraph-api: {body}", exc_info=e)
raise e
- async for event in EventSource(sse.response).aiter_sse():
- yield StreamPart(
- event.event, orjson.loads(event.data) if event.data else None
+ # check content type
+ content_type = self._response.headers.get("content-type", "").partition(
+ ";"
+ )[0]
+ if "text/event-stream" not in content_type:
+ raise httpx.TransportError(
+ "Expected response header Content-Type to contain 'text/event-stream', "
+ f"got {content_type!r}"
)
+ # parse SSE
+ decoder = SSEDecoder()
+ async for line in aiter_lines_raw(res):
+ sse = decoder.decode(line=line.rstrip(b"\n"))
+ if sse is not None:
+ yield sse
async def aencode_json(json: Any) -> tuple[dict[str, str], bytes]:
@@ -2421,22 +2435,32 @@ class SyncHttpClient:
) -> Iterator[StreamPart]:
"""Stream the results of a request using SSE."""
headers, content = encode_json(json)
- with httpx_sse.connect_sse(
- self.client, method, path, headers=headers, content=content
- ) as sse:
+ with self.client.stream(method, path, headers=headers, content=content) as res:
+ # check status
try:
- sse.response.raise_for_status()
+ res.raise_for_status()
except httpx.HTTPStatusError as e:
- body = sse.response.read().decode()
+ body = (res.read()).decode()
if sys.version_info >= (3, 11):
e.add_note(body)
else:
logger.error(f"Error from langgraph-api: {body}", exc_info=e)
raise e
- for event in EventSource(sse.response).iter_sse():
- yield StreamPart(
- event.event, orjson.loads(event.data) if event.data else None
+ # check content type
+ content_type = self._response.headers.get("content-type", "").partition(
+ ";"
+ )[0]
+ if "text/event-stream" not in content_type:
+ raise httpx.TransportError(
+ "Expected response header Content-Type to contain 'text/event-stream', "
+ f"got {content_type!r}"
)
+ # parse SSE
+ decoder = SSEDecoder()
+ for line in iter_lines_raw(res):
+ sse = decoder.decode(line.rstrip(b"\n"))
+ if sse is not None:
+ yield sse
def encode_json(json: Any) -> tuple[dict[str, str], bytes]:
diff --git a/libs/sdk-py/langgraph_sdk/sse.py b/libs/sdk-py/langgraph_sdk/sse.py
index 8b019e4a6..6460b363c 100644
--- a/libs/sdk-py/langgraph_sdk/sse.py
+++ b/libs/sdk-py/langgraph_sdk/sse.py
@@ -1,11 +1,13 @@
"""Adapted from httpx_sse to split lines on \n, \r, \r\n per the SSE spec."""
-import io
-from typing import AsyncIterator, Iterator
+from typing import AsyncIterator, Iterator, Optional, Union
import httpx
-import httpx_sse
-import httpx_sse._decoders
+import orjson
+
+from langgraph_sdk.schema import StreamPart
+
+BytesLike = Union[bytes, bytearray, memoryview]
class BytesLineDecoder:
@@ -17,10 +19,10 @@ class BytesLineDecoder:
"""
def __init__(self) -> None:
- self.buffer = io.BytesIO()
+ self.buffer = bytearray()
self.trailing_cr: bool = False
- def decode(self, text: bytes) -> list[bytes]:
+ def decode(self, text: bytes) -> list[BytesLike]:
# See https://docs.python.org/3/glossary.html#term-universal-newlines
NEWLINE_CHARS = b"\n\r"
@@ -42,33 +44,93 @@ class BytesLineDecoder:
if len(lines) == 1 and not trailing_newline:
# No new lines, buffer the input and continue.
- self.buffer.write(lines[0])
+ self.buffer.extend(lines[0])
return []
if self.buffer:
# Include any existing buffer in the first portion of the
# splitlines result.
- lines = [self.buffer.getvalue() + lines[0]] + lines[1:]
- self.buffer.truncate(0)
+ self.buffer.extend(lines[0])
+ lines = [self.buffer] + lines[1:]
+ self.buffer = bytearray()
if not trailing_newline:
# If the last segment of splitlines is not newline terminated,
# then drop it from our output and start a new buffer.
- self.buffer.write(lines.pop())
+ self.buffer.extend(lines.pop())
return lines
- def flush(self) -> list[bytes]:
+ def flush(self) -> list[BytesLike]:
if not self.buffer and not self.trailing_cr:
return []
- lines = [self.buffer.getvalue()] if self.buffer else []
- self.buffer.truncate(0)
+ lines = [self.buffer]
+ self.buffer = bytearray()
self.trailing_cr = False
return lines
-async def aiter_lines_raw(response: httpx.Response) -> AsyncIterator[bytes]:
+class SSEDecoder:
+ def __init__(self) -> None:
+ self._event = ""
+ self._data = bytearray()
+ self._last_event_id = ""
+ self._retry: Optional[int] = None
+
+ def decode(self, line: bytes) -> Optional[StreamPart]:
+ # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501
+
+ if not line:
+ if (
+ not self._event
+ and not self._data
+ and not self._last_event_id
+ and self._retry is None
+ ):
+ return None
+
+ sse = StreamPart(
+ event=self._event,
+ data=orjson.loads(self._data) if self._data else None,
+ )
+
+ # NOTE: as per the SSE spec, do not reset last_event_id.
+ self._event = ""
+ self._data = bytearray()
+ self._retry = None
+
+ return sse
+
+ if line.startswith(b":"):
+ return None
+
+ fieldname, _, value = line.partition(b":")
+
+ if value.startswith(b" "):
+ value = value[1:]
+
+ if fieldname == b"event":
+ self._event = value.decode()
+ elif fieldname == b"data":
+ self._data.extend(value)
+ elif fieldname == b"id":
+ if b"\0" in value:
+ pass
+ else:
+ self._last_event_id = value.decode()
+ elif fieldname == b"retry":
+ try:
+ self._retry = int(value)
+ except (TypeError, ValueError):
+ pass
+ else:
+ pass # Field is ignored.
+
+ return None
+
+
+async def aiter_lines_raw(response: httpx.Response) -> AsyncIterator[BytesLike]:
decoder = BytesLineDecoder()
async for chunk in response.aiter_bytes():
for line in decoder.decode(chunk):
@@ -77,30 +139,10 @@ async def aiter_lines_raw(response: httpx.Response) -> AsyncIterator[bytes]:
yield line
-def iter_lines_raw(response: httpx.Response) -> Iterator[bytes]:
+def iter_lines_raw(response: httpx.Response) -> Iterator[BytesLike]:
decoder = BytesLineDecoder()
for chunk in response.iter_bytes():
for line in decoder.decode(chunk):
yield line
for line in decoder.flush():
yield line
-
-
-class EventSource(httpx_sse.EventSource):
- async def aiter_sse(self) -> AsyncIterator[httpx_sse.ServerSentEvent]:
- self._check_content_type()
- decoder = httpx_sse._decoders.SSEDecoder()
- async for line in aiter_lines_raw(self._response):
- line = line.rstrip(b"\n")
- sse = decoder.decode(line.decode())
- if sse is not None:
- yield sse
-
- def iter_sse(self) -> Iterator[httpx_sse.ServerSentEvent]:
- self._check_content_type()
- decoder = httpx_sse._decoders.SSEDecoder()
- for line in iter_lines_raw(self._response):
- line = line.rstrip(b"\n")
- sse = decoder.decode(line.decode())
- if sse is not None:
- yield sse
diff --git a/libs/sdk-py/poetry.lock b/libs/sdk-py/poetry.lock
index 5bff56b98..1024f9799 100644
--- a/libs/sdk-py/poetry.lock
+++ b/libs/sdk-py/poetry.lock
@@ -141,17 +141,6 @@ cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
-[[package]]
-name = "httpx-sse"
-version = "0.4.0"
-description = "Consume Server-Sent Event (SSE) messages with HTTPX."
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"},
- {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"},
-]
-
[[package]]
name = "idna"
version = "3.7"
@@ -490,4 +479,4 @@ watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0,<4.0"
-content-hash = "832acea0ad21ce71ae74edef225a1ad6f8fb166f6bf1531d876fe80fac7495f0"
+content-hash = "1262a6148df18cc44ade00466b6e0f8305897a460eea370c8de649d8d20cd7a2"
diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml
index c7776a6cc..678925c5b 100644
--- a/libs/sdk-py/pyproject.toml
+++ b/libs/sdk-py/pyproject.toml
@@ -11,7 +11,6 @@ packages = [{ include = "langgraph_sdk" }]
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
httpx = ">=0.25.2"
-httpx-sse = ">=0.4.0"
orjson = ">=3.10.1"
[tool.poetry.group.dev.dependencies]
From 2b65308508fa829198cef3ca547b61b46ca8142e Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Fri, 15 Nov 2024 14:01:16 -0800
Subject: [PATCH 080/149] WIP: Handle commands for subgraphs
---
libs/langgraph/tests/test_pregel.py | 32 +++++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py
index c2ed63d28..d45c3ae22 100644
--- a/libs/langgraph/tests/test_pregel.py
+++ b/libs/langgraph/tests/test_pregel.py
@@ -14471,3 +14471,35 @@ def test_parent_command(request: pytest.FixtureRequest, checkpointer_name: str)
},
tasks=(),
)
+
+
+@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
+def test_interrupt_subgraph(request: pytest.FixtureRequest, checkpointer_name: str):
+ checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
+
+ class State(TypedDict):
+ baz: str
+
+ def foo(state):
+ return {"baz": "foo"}
+
+ def bar(state):
+ value = interrupt("Please provide baz value:")
+ return {"baz": value}
+
+ child_builder = StateGraph(State)
+ child_builder.add_node(bar)
+ child_builder.add_edge(START, "bar")
+
+ builder = StateGraph(State)
+ builder.add_node(foo)
+ builder.add_node("bar", child_builder.compile())
+ builder.add_edge(START, "foo")
+ builder.add_edge("foo", "bar")
+ graph = builder.compile(checkpointer=checkpointer)
+
+ thread1 = {"configurable": {"thread_id": "1"}}
+ # First run, interrupted at bar
+ assert graph.invoke({"baz": ""}, thread1)
+ # Resume with answer
+ assert graph.invoke(Command(resume="bar"), thread1)
From 6fc1c602ab151525904fcec033f327f75b0db34e Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Mon, 2 Dec 2024 13:53:11 -0800
Subject: [PATCH 081/149] Add one more
---
libs/langgraph/tests/test_pregel.py | 153 ++++++++++++++++++++++++++++
1 file changed, 153 insertions(+)
diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py
index d45c3ae22..113a6c3c8 100644
--- a/libs/langgraph/tests/test_pregel.py
+++ b/libs/langgraph/tests/test_pregel.py
@@ -8728,6 +8728,159 @@ def test_copy_checkpoint(
)
+@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
+def test_dynamic_interrupt_subgraph(
+ request: pytest.FixtureRequest, checkpointer_name: str
+) -> None:
+ checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
+
+ class SubgraphState(TypedDict):
+ my_key: str
+ market: str
+
+ tool_two_node_count = 0
+
+ def tool_two_node(s: SubgraphState) -> SubgraphState:
+ print("tool_two_node", s)
+ nonlocal tool_two_node_count
+ tool_two_node_count += 1
+ if s["market"] == "DE":
+ answer = interrupt("Just because...")
+ else:
+ answer = " all good"
+ print("after interrupt")
+ return {"my_key": answer}
+
+ subgraph = StateGraph(SubgraphState)
+ subgraph.add_node("do", tool_two_node, retry=RetryPolicy())
+ subgraph.add_edge(START, "do")
+
+ class State(TypedDict):
+ my_key: Annotated[str, operator.add]
+ market: str
+
+ tool_two_graph = StateGraph(State)
+ tool_two_graph.add_node("tool_two", subgraph.compile())
+ tool_two_graph.add_edge(START, "tool_two")
+ tool_two = tool_two_graph.compile()
+
+ tracer = FakeTracer()
+ assert tool_two.invoke(
+ {"my_key": "value", "market": "DE"}, {"callbacks": [tracer]}
+ ) == {
+ "my_key": "value",
+ "market": "DE",
+ }
+ assert tool_two_node_count == 1, "interrupts aren't retried"
+ assert len(tracer.runs) == 1
+ run = tracer.runs[0]
+ assert run.end_time is not None
+ assert run.error is None
+ assert run.outputs == {"market": "DE", "my_key": "value"}
+
+ assert tool_two.invoke({"my_key": "value", "market": "US"}) == {
+ "my_key": "value all good",
+ "market": "US",
+ }
+
+ tool_two = tool_two_graph.compile(checkpointer=checkpointer)
+
+ # missing thread_id
+ with pytest.raises(ValueError, match="thread_id"):
+ tool_two.invoke({"my_key": "value", "market": "DE"})
+
+ # flow: interrupt -> resume with answer
+ thread2 = {"configurable": {"thread_id": "2"}}
+ # stop when about to enter node
+ assert [
+ c for c in tool_two.stream({"my_key": "value ⛰️", "market": "DE"}, thread2)
+ ] == [
+ {
+ "__interrupt__": (
+ Interrupt(
+ value="Just because...",
+ resumable=True,
+ ns=[AnyStr("tool_two:"), AnyStr("do:")],
+ ),
+ )
+ },
+ ]
+ # resume with answer
+ assert [c for c in tool_two.stream(Command(resume=" my answer"), thread2)] == [
+ {"tool_two": {"my_key": " my answer"}},
+ ]
+
+ # flow: interrupt -> clear tasks
+ thread1 = {"configurable": {"thread_id": "1"}}
+ # stop when about to enter node
+ assert tool_two.invoke({"my_key": "value ⛰️", "market": "DE"}, thread1) == {
+ "my_key": "value ⛰️",
+ "market": "DE",
+ }
+ assert [c.metadata for c in tool_two.checkpointer.list(thread1)] == [
+ {
+ "parents": {},
+ "source": "loop",
+ "step": 0,
+ "writes": None,
+ "thread_id": "1",
+ },
+ {
+ "parents": {},
+ "source": "input",
+ "step": -1,
+ "writes": {"__start__": {"my_key": "value ⛰️", "market": "DE"}},
+ "thread_id": "1",
+ },
+ ]
+ assert tool_two.get_state(thread1) == StateSnapshot(
+ values={"my_key": "value ⛰️", "market": "DE"},
+ next=("tool_two",),
+ tasks=(
+ PregelTask(
+ AnyStr(),
+ "tool_two",
+ (PULL, "tool_two"),
+ interrupts=(
+ Interrupt(
+ value="Just because...",
+ resumable=True,
+ ns=[AnyStr("tool_two:")],
+ ),
+ ),
+ ),
+ ),
+ config=tool_two.checkpointer.get_tuple(thread1).config,
+ created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"],
+ metadata={
+ "parents": {},
+ "source": "loop",
+ "step": 0,
+ "writes": None,
+ "thread_id": "1",
+ },
+ parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config,
+ )
+ # clear the interrupt and next tasks
+ tool_two.update_state(thread1, None, as_node=END)
+ # interrupt and next tasks are cleared
+ assert tool_two.get_state(thread1) == StateSnapshot(
+ values={"my_key": "value ⛰️", "market": "DE"},
+ next=(),
+ tasks=(),
+ config=tool_two.checkpointer.get_tuple(thread1).config,
+ created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"],
+ metadata={
+ "parents": {},
+ "source": "update",
+ "step": 1,
+ "writes": {},
+ "thread_id": "1",
+ },
+ parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config,
+ )
+
+
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
def test_start_branch_then(
snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str
From 988dd237d26be56133718f06f91da107836cd982 Mon Sep 17 00:00:00 2001
From: Tat Dat Duong
Date: Tue, 3 Dec 2024 01:17:15 +0100
Subject: [PATCH 082/149] feat(sdk): pass cancel on disconnect when joining
stream
---
libs/sdk-js/src/client.ts | 17 +++++++++++++----
libs/sdk-py/langgraph_sdk/client.py | 11 +++++++++--
2 files changed, 22 insertions(+), 6 deletions(-)
diff --git a/libs/sdk-js/src/client.ts b/libs/sdk-js/src/client.ts
index ecf7147ca..14284bcff 100644
--- a/libs/sdk-js/src/client.ts
+++ b/libs/sdk-js/src/client.ts
@@ -1021,19 +1021,28 @@ export class RunsClient extends BaseClient {
*
* @param threadId The ID of the thread.
* @param runId The ID of the run.
- * @param signal An optional abort signal.
* @returns An async generator yielding stream parts.
*/
async *joinStream(
threadId: string,
runId: string,
- signal?: AbortSignal,
+ options?:
+ | { signal?: AbortSignal; cancelOnDisconnect?: boolean }
+ | AbortSignal,
): AsyncGenerator<{ event: StreamEvent; data: any }> {
+ const opts =
+ typeof options === "object" &&
+ options != null &&
+ options instanceof AbortSignal
+ ? { signal: options }
+ : options;
+
const response = await this.asyncCaller.fetch(
...this.prepareFetchOptions(`/threads/${threadId}/runs/${runId}/stream`, {
method: "GET",
timeoutMs: null,
- signal,
+ signal: opts?.signal,
+ params: { cancel_on_disconnect: opts?.cancelOnDisconnect ? "1" : "0" },
}),
);
@@ -1048,7 +1057,7 @@ export class RunsClient extends BaseClient {
async start(ctrl) {
parser = createParser((event) => {
if (
- (signal && signal.aborted) ||
+ (opts?.signal && opts.signal.aborted) ||
(event.type === "event" && event.data === "[DONE]")
) {
ctrl.terminate();
diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py
index b7cc1a78c..7a959a477 100644
--- a/libs/sdk-py/langgraph_sdk/client.py
+++ b/libs/sdk-py/langgraph_sdk/client.py
@@ -1796,7 +1796,9 @@ class RunsClient:
""" # noqa: E501
return await self.http.get(f"/threads/{thread_id}/runs/{run_id}/join")
- def join_stream(self, thread_id: str, run_id: str) -> AsyncIterator[StreamPart]:
+ def join_stream(
+ self, thread_id: str, run_id: str, *, cancel_on_disconnect: bool = False
+ ) -> AsyncIterator[StreamPart]:
"""Stream output from a run in real-time, until the run is done.
Output is not buffered, so any output produced before this call will
not be received here.
@@ -1804,6 +1806,7 @@ class RunsClient:
Args:
thread_id: The thread ID to join.
run_id: The run ID to join.
+ cancel_on_disconnect: Whether to cancel the run when the stream is disconnected.
Returns:
None
@@ -1816,7 +1819,11 @@ class RunsClient:
)
""" # noqa: E501
- return self.http.stream(f"/threads/{thread_id}/runs/{run_id}/stream", "GET")
+ return self.http.stream(
+ f"/threads/{thread_id}/runs/{run_id}/stream",
+ "GET",
+ params={"cancel_on_disconnect": cancel_on_disconnect},
+ )
async def delete(self, thread_id: str, run_id: str) -> None:
"""Delete a run.
From 6a6c3ed84cfaaea6000ddcb1c6c9bc8267d15eea Mon Sep 17 00:00:00 2001
From: Tat Dat Duong
Date: Tue, 3 Dec 2024 01:31:11 +0100
Subject: [PATCH 083/149] Bump to 0.0.30
---
libs/sdk-js/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/sdk-js/package.json b/libs/sdk-js/package.json
index ded99a00b..3b373dabe 100644
--- a/libs/sdk-js/package.json
+++ b/libs/sdk-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
- "version": "0.0.29",
+ "version": "0.0.30",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
From a91bf116cbfae4aa37a6c69b3b4a66795485f043 Mon Sep 17 00:00:00 2001
From: Tat Dat Duong
Date: Tue, 3 Dec 2024 01:31:55 +0100
Subject: [PATCH 084/149] Bump to 0.1.41
---
libs/sdk-py/pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml
index c7776a6cc..46464c6a6 100644
--- a/libs/sdk-py/pyproject.toml
+++ b/libs/sdk-py/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
-version = "0.1.40"
+version = "0.1.41"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"
From efbd02a27da9abdb15d2c31aeb1c76ec2a430ccd Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Mon, 2 Dec 2024 16:43:35 -0800
Subject: [PATCH 085/149] Implement support for interrupt/resume in subgraphs
---
libs/langgraph/langgraph/pregel/algo.py | 4 +-
libs/langgraph/tests/test_pregel.py | 31 +++-
libs/langgraph/tests/test_pregel_async.py | 215 ++++++++++++++++++++++
3 files changed, 241 insertions(+), 9 deletions(-)
diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py
index 3c3008ce4..5d104a85f 100644
--- a/libs/langgraph/langgraph/pregel/algo.py
+++ b/libs/langgraph/langgraph/pregel/algo.py
@@ -595,7 +595,7 @@ def prepare_single_task(
for tid, c, v in pending_writes
if tid in (NULL_TASK_ID, task_id) and c == RESUME
),
- MISSING,
+ configurable.get(CONFIG_KEY_RESUME_VALUE, MISSING),
),
},
),
@@ -720,7 +720,7 @@ def prepare_single_task(
if tid in (NULL_TASK_ID, task_id)
and c == RESUME
),
- MISSING,
+ configurable.get(CONFIG_KEY_RESUME_VALUE, MISSING),
),
},
),
diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py
index 113a6c3c8..6ff70276f 100644
--- a/libs/langgraph/tests/test_pregel.py
+++ b/libs/langgraph/tests/test_pregel.py
@@ -8741,14 +8741,12 @@ def test_dynamic_interrupt_subgraph(
tool_two_node_count = 0
def tool_two_node(s: SubgraphState) -> SubgraphState:
- print("tool_two_node", s)
nonlocal tool_two_node_count
tool_two_node_count += 1
if s["market"] == "DE":
answer = interrupt("Just because...")
else:
answer = " all good"
- print("after interrupt")
return {"my_key": answer}
subgraph = StateGraph(SubgraphState)
@@ -8807,7 +8805,7 @@ def test_dynamic_interrupt_subgraph(
]
# resume with answer
assert [c for c in tool_two.stream(Command(resume=" my answer"), thread2)] == [
- {"tool_two": {"my_key": " my answer"}},
+ {"tool_two": {"my_key": " my answer", "market": "DE"}},
]
# flow: interrupt -> clear tasks
@@ -8817,7 +8815,12 @@ def test_dynamic_interrupt_subgraph(
"my_key": "value ⛰️",
"market": "DE",
}
- assert [c.metadata for c in tool_two.checkpointer.list(thread1)] == [
+ assert [
+ c.metadata
+ for c in tool_two.checkpointer.list(
+ {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
+ )
+ ] == [
{
"parents": {},
"source": "loop",
@@ -8845,9 +8848,15 @@ def test_dynamic_interrupt_subgraph(
Interrupt(
value="Just because...",
resumable=True,
- ns=[AnyStr("tool_two:")],
+ ns=[AnyStr("tool_two:"), AnyStr("do:")],
),
),
+ state={
+ "configurable": {
+ "thread_id": "1",
+ "checkpoint_ns": AnyStr("tool_two:"),
+ }
+ },
),
),
config=tool_two.checkpointer.get_tuple(thread1).config,
@@ -8859,7 +8868,11 @@ def test_dynamic_interrupt_subgraph(
"writes": None,
"thread_id": "1",
},
- parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config,
+ parent_config=[
+ *tool_two.checkpointer.list(
+ {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}, limit=2
+ )
+ ][-1].config,
)
# clear the interrupt and next tasks
tool_two.update_state(thread1, None, as_node=END)
@@ -8877,7 +8890,11 @@ def test_dynamic_interrupt_subgraph(
"writes": {},
"thread_id": "1",
},
- parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config,
+ parent_config=[
+ *tool_two.checkpointer.list(
+ {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}, limit=2
+ )
+ ][-1].config,
)
diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py
index 813fa8240..6c85e57ed 100644
--- a/libs/langgraph/tests/test_pregel_async.py
+++ b/libs/langgraph/tests/test_pregel_async.py
@@ -429,6 +429,189 @@ async def test_dynamic_interrupt(checkpointer_name: str) -> None:
)
+@pytest.mark.skipif(
+ sys.version_info < (3, 11),
+ reason="Python 3.11+ is required for async contextvars support",
+)
+@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
+async def test_dynamic_interrupt_subgraph(checkpointer_name: str) -> None:
+ class SubgraphState(TypedDict):
+ my_key: str
+ market: str
+
+ tool_two_node_count = 0
+
+ def tool_two_node(s: SubgraphState) -> SubgraphState:
+ nonlocal tool_two_node_count
+ tool_two_node_count += 1
+ if s["market"] == "DE":
+ answer = interrupt("Just because...")
+ else:
+ answer = " all good"
+ return {"my_key": answer}
+
+ subgraph = StateGraph(SubgraphState)
+ subgraph.add_node("do", tool_two_node, retry=RetryPolicy())
+ subgraph.add_edge(START, "do")
+
+ class State(TypedDict):
+ my_key: Annotated[str, operator.add]
+ market: str
+
+ tool_two_graph = StateGraph(State)
+ tool_two_graph.add_node("tool_two", subgraph.compile())
+ tool_two_graph.add_edge(START, "tool_two")
+ tool_two = tool_two_graph.compile()
+
+ tracer = FakeTracer()
+ assert await tool_two.ainvoke(
+ {"my_key": "value", "market": "DE"}, {"callbacks": [tracer]}
+ ) == {
+ "my_key": "value",
+ "market": "DE",
+ }
+ assert tool_two_node_count == 1, "interrupts aren't retried"
+ assert len(tracer.runs) == 1
+ run = tracer.runs[0]
+ assert run.end_time is not None
+ assert run.error is None
+ assert run.outputs == {"market": "DE", "my_key": "value"}
+
+ assert await tool_two.ainvoke({"my_key": "value", "market": "US"}) == {
+ "my_key": "value all good",
+ "market": "US",
+ }
+
+ async with awith_checkpointer(checkpointer_name) as checkpointer:
+ tool_two = tool_two_graph.compile(checkpointer=checkpointer)
+
+ # missing thread_id
+ with pytest.raises(ValueError, match="thread_id"):
+ await tool_two.ainvoke({"my_key": "value", "market": "DE"})
+
+ # flow: interrupt -> resume with answer
+ thread2 = {"configurable": {"thread_id": "2"}}
+ # stop when about to enter node
+ assert [
+ c
+ async for c in tool_two.astream(
+ {"my_key": "value ⛰️", "market": "DE"}, thread2
+ )
+ ] == [
+ {
+ "__interrupt__": (
+ Interrupt(
+ value="Just because...",
+ resumable=True,
+ ns=[AnyStr("tool_two:"), AnyStr("do:")],
+ ),
+ )
+ },
+ ]
+ # resume with answer
+ assert [
+ c async for c in tool_two.astream(Command(resume=" my answer"), thread2)
+ ] == [
+ {"tool_two": {"my_key": " my answer", "market": "DE"}},
+ ]
+
+ # flow: interrupt -> clear
+ thread1 = {"configurable": {"thread_id": "1"}}
+ thread1root = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
+ # stop when about to enter node
+ assert [
+ c
+ async for c in tool_two.astream(
+ {"my_key": "value ⛰️", "market": "DE"}, thread1
+ )
+ ] == [
+ {
+ "__interrupt__": (
+ Interrupt(
+ value="Just because...",
+ resumable=True,
+ ns=[AnyStr("tool_two:"), AnyStr("do:")],
+ ),
+ )
+ },
+ ]
+ assert [c.metadata async for c in tool_two.checkpointer.alist(thread1root)] == [
+ {
+ "parents": {},
+ "source": "loop",
+ "step": 0,
+ "writes": None,
+ "thread_id": "1",
+ },
+ {
+ "parents": {},
+ "source": "input",
+ "step": -1,
+ "writes": {"__start__": {"my_key": "value ⛰️", "market": "DE"}},
+ "thread_id": "1",
+ },
+ ]
+ tup = await tool_two.checkpointer.aget_tuple(thread1)
+ assert await tool_two.aget_state(thread1) == StateSnapshot(
+ values={"my_key": "value ⛰️", "market": "DE"},
+ next=("tool_two",),
+ tasks=(
+ PregelTask(
+ AnyStr(),
+ "tool_two",
+ (PULL, "tool_two"),
+ interrupts=(
+ Interrupt(
+ value="Just because...",
+ resumable=True,
+ ns=[AnyStr("tool_two:"), AnyStr("do:")],
+ ),
+ ),
+ state={
+ "configurable": {
+ "thread_id": "1",
+ "checkpoint_ns": AnyStr("tool_two:"),
+ }
+ },
+ ),
+ ),
+ config=tup.config,
+ created_at=tup.checkpoint["ts"],
+ metadata={
+ "parents": {},
+ "source": "loop",
+ "step": 0,
+ "writes": None,
+ "thread_id": "1",
+ },
+ parent_config=[
+ c async for c in tool_two.checkpointer.alist(thread1root, limit=2)
+ ][-1].config,
+ )
+
+ # clear the interrupt and next tasks
+ await tool_two.aupdate_state(thread1, None, as_node=END)
+ # interrupt is cleared, as well as the next tasks
+ tup = await tool_two.checkpointer.aget_tuple(thread1)
+ assert await tool_two.aget_state(thread1) == StateSnapshot(
+ values={"my_key": "value ⛰️", "market": "DE"},
+ next=(),
+ tasks=(),
+ config=tup.config,
+ created_at=tup.checkpoint["ts"],
+ metadata={
+ "parents": {},
+ "source": "update",
+ "step": 1,
+ "writes": {},
+ "thread_id": "1",
+ },
+ parent_config=[
+ c async for c in tool_two.checkpointer.alist(thread1root, limit=2)
+ ][-1].config,
+ )
+
+
@pytest.mark.skipif(not FF_SEND_V2, reason="send v2 is not enabled")
@pytest.mark.skipif(
sys.version_info < (3, 11),
@@ -12677,3 +12860,35 @@ async def test_parent_command(checkpointer_name: str) -> None:
},
tasks=(),
)
+
+
+@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
+async def test_interrupt_subgraph(checkpointer_name: str):
+ class State(TypedDict):
+ baz: str
+
+ def foo(state):
+ return {"baz": "foo"}
+
+ def bar(state):
+ value = interrupt("Please provide baz value:")
+ return {"baz": value}
+
+ child_builder = StateGraph(State)
+ child_builder.add_node(bar)
+ child_builder.add_edge(START, "bar")
+
+ builder = StateGraph(State)
+ builder.add_node(foo)
+ builder.add_node("bar", child_builder.compile())
+ builder.add_edge(START, "foo")
+ builder.add_edge("foo", "bar")
+
+ async with awith_checkpointer(checkpointer_name) as checkpointer:
+ graph = builder.compile(checkpointer=checkpointer)
+
+ thread1 = {"configurable": {"thread_id": "1"}}
+ # First run, interrupted at bar
+ assert await graph.ainvoke({"baz": ""}, thread1)
+ # Resume with answer
+ assert await graph.ainvoke(Command(resume="bar"), thread1)
From a3feaef2eb0a25ccdc0abbeedd8107c2b1b8e3b8 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Mon, 2 Dec 2024 16:45:07 -0800
Subject: [PATCH 086/149] lib: Handle Command in RemoteGraph
---
libs/langgraph/langgraph/pregel/remote.py | 15 ++++++++++++++-
libs/sdk-py/langgraph_sdk/client.py | 15 +++++++++++++++
2 files changed, 29 insertions(+), 1 deletion(-)
diff --git a/libs/langgraph/langgraph/pregel/remote.py b/libs/langgraph/langgraph/pregel/remote.py
index abe27eb28..8ee7ec884 100644
--- a/libs/langgraph/langgraph/pregel/remote.py
+++ b/libs/langgraph/langgraph/pregel/remote.py
@@ -1,3 +1,4 @@
+from dataclasses import asdict
from typing import (
Any,
AsyncIterator,
@@ -41,7 +42,7 @@ from langgraph.constants import (
from langgraph.errors import GraphInterrupt
from langgraph.pregel.protocol import PregelProtocol
from langgraph.pregel.types import All, PregelTask, StateSnapshot, StreamMode
-from langgraph.types import Interrupt, StreamProtocol
+from langgraph.types import Command, Interrupt, StreamProtocol
from langgraph.utils.config import merge_configs
@@ -597,11 +598,17 @@ class RemoteGraph(PregelProtocol):
stream_modes, requested, req_single, stream = self._get_stream_modes(
stream_mode, config
)
+ if isinstance(input, Command):
+ command: dict[str, Any] = asdict(input)
+ input = None
+ else:
+ command = None
for chunk in sync_client.runs.stream(
thread_id=sanitized_config["configurable"].get("thread_id"),
assistant_id=self.name,
input=input,
+ command=command,
config=sanitized_config,
stream_mode=stream_modes,
interrupt_before=interrupt_before,
@@ -680,11 +687,17 @@ class RemoteGraph(PregelProtocol):
stream_modes, requested, req_single, stream = self._get_stream_modes(
stream_mode, config
)
+ if isinstance(input, Command):
+ command: dict[str, Any] = asdict(input)
+ input = None
+ else:
+ command = None
async for chunk in client.runs.stream(
thread_id=sanitized_config["configurable"].get("thread_id"),
assistant_id=self.name,
input=input,
+ command=command,
config=sanitized_config,
stream_mode=stream_modes,
interrupt_before=interrupt_before,
diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py
index 81e8a8506..be909d336 100644
--- a/libs/sdk-py/langgraph_sdk/client.py
+++ b/libs/sdk-py/langgraph_sdk/client.py
@@ -3303,6 +3303,7 @@ class SyncRunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
+ command: Optional[Command] = None,
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
stream_subgraphs: bool = False,
metadata: Optional[dict] = None,
@@ -3326,6 +3327,7 @@ class SyncRunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
+ command: Optional[Command] = None,
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
stream_subgraphs: bool = False,
metadata: Optional[dict] = None,
@@ -3346,6 +3348,7 @@ class SyncRunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
+ command: Optional[Command] = None,
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
stream_subgraphs: bool = False,
metadata: Optional[dict] = None,
@@ -3370,6 +3373,7 @@ class SyncRunsClient:
assistant_id: The assistant ID or graph name to stream from.
If using graph name, will default to first assistant created from that graph.
input: The input to the graph.
+ command: The command to execute.
stream_mode: The stream mode(s) to use.
stream_subgraphs: Whether to stream output from subgraphs.
metadata: Metadata to assign to the run.
@@ -3420,6 +3424,7 @@ class SyncRunsClient:
""" # noqa: E501
payload = {
"input": input,
+ "command": command,
"config": config,
"metadata": metadata,
"stream_mode": stream_mode,
@@ -3453,6 +3458,7 @@ class SyncRunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
+ command: Optional[Command] = None,
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
stream_subgraphs: bool = False,
metadata: Optional[dict] = None,
@@ -3472,6 +3478,7 @@ class SyncRunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
+ command: Optional[Command] = None,
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
stream_subgraphs: bool = False,
metadata: Optional[dict] = None,
@@ -3492,6 +3499,7 @@ class SyncRunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
+ command: Optional[Command] = None,
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
stream_subgraphs: bool = False,
metadata: Optional[dict] = None,
@@ -3514,6 +3522,7 @@ class SyncRunsClient:
assistant_id: The assistant ID or graph name to stream from.
If using graph name, will default to first assistant created from that graph.
input: The input to the graph.
+ command: The command to execute.
stream_mode: The stream mode(s) to use.
stream_subgraphs: Whether to stream output from subgraphs.
metadata: Metadata to assign to the run.
@@ -3600,6 +3609,7 @@ class SyncRunsClient:
""" # noqa: E501
payload = {
"input": input,
+ "command": command,
"stream_mode": stream_mode,
"stream_subgraphs": stream_subgraphs,
"config": config,
@@ -3637,6 +3647,7 @@ class SyncRunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
+ command: Optional[Command] = None,
metadata: Optional[dict] = None,
config: Optional[Config] = None,
checkpoint: Optional[Checkpoint] = None,
@@ -3657,6 +3668,7 @@ class SyncRunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
+ command: Optional[Command] = None,
metadata: Optional[dict] = None,
config: Optional[Config] = None,
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
@@ -3674,6 +3686,7 @@ class SyncRunsClient:
assistant_id: str,
*,
input: Optional[dict] = None,
+ command: Optional[Command] = None,
metadata: Optional[dict] = None,
config: Optional[Config] = None,
checkpoint: Optional[Checkpoint] = None,
@@ -3695,6 +3708,7 @@ class SyncRunsClient:
assistant_id: The assistant ID or graph name to run.
If using graph name, will default to first assistant created from that graph.
input: The input to the graph.
+ command: The command to execute.
metadata: Metadata to assign to the run.
config: The configuration for the assistant.
checkpoint: The checkpoint to resume from.
@@ -3761,6 +3775,7 @@ class SyncRunsClient:
""" # noqa: E501
payload = {
"input": input,
+ "command": command,
"config": config,
"metadata": metadata,
"assistant_id": assistant_id,
From d36e6ceaaf3c9c9d57ae751bf1ab9e965fec370e Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Mon, 2 Dec 2024 16:59:38 -0800
Subject: [PATCH 087/149] Fix
---
libs/langgraph/tests/test_pregel_async.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py
index 6c85e57ed..378ca1ac1 100644
--- a/libs/langgraph/tests/test_pregel_async.py
+++ b/libs/langgraph/tests/test_pregel_async.py
@@ -12862,6 +12862,10 @@ async def test_parent_command(checkpointer_name: str) -> None:
)
+@pytest.mark.skipif(
+ sys.version_info < (3, 11),
+ reason="Python 3.11+ is required for async contextvars support",
+)
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
async def test_interrupt_subgraph(checkpointer_name: str):
class State(TypedDict):
From 0071bd1e1cf91f84a9c364ee3eae8eacd9ad1001 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Mon, 2 Dec 2024 17:01:12 -0800
Subject: [PATCH 088/149] Lint
---
libs/langgraph/langgraph/pregel/remote.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/libs/langgraph/langgraph/pregel/remote.py b/libs/langgraph/langgraph/pregel/remote.py
index 8ee7ec884..d45cdb310 100644
--- a/libs/langgraph/langgraph/pregel/remote.py
+++ b/libs/langgraph/langgraph/pregel/remote.py
@@ -28,6 +28,7 @@ from langgraph_sdk.client import (
get_sync_client,
)
from langgraph_sdk.schema import Checkpoint, ThreadState
+from langgraph_sdk.schema import Command as CommandSDK
from langgraph_sdk.schema import StreamMode as StreamModeSDK
from typing_extensions import Self
@@ -599,7 +600,7 @@ class RemoteGraph(PregelProtocol):
stream_mode, config
)
if isinstance(input, Command):
- command: dict[str, Any] = asdict(input)
+ command: Optional[CommandSDK] = cast(CommandSDK, asdict(input))
input = None
else:
command = None
@@ -688,7 +689,7 @@ class RemoteGraph(PregelProtocol):
stream_mode, config
)
if isinstance(input, Command):
- command: dict[str, Any] = asdict(input)
+ command: Optional[CommandSDK] = cast(CommandSDK, asdict(input))
input = None
else:
command = None
From 20f091a27710f9c0b0a799786f73bc73b13daaa9 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Mon, 2 Dec 2024 17:08:24 -0800
Subject: [PATCH 089/149] [postgres] Sort Ascending (#2594)
Adds a few of preliminaries:
1. Makes the returned "score" actually the result of the requested
operation (cosine, inner_product, l2)
2. Sorts asc, etc. so that if you were to add an HNSW index (and not
have any WHERE filters), it would be used
3. Drop the inner WHERE statement if no namespace or other filters are
provided. See (2) for why.
I don't yet add an index to the migrations since I think we need to
agree on the right balance to ensure it's actually used in common query
patterns.
---
.../langgraph/store/postgres/base.py | 43 ++++++---
libs/checkpoint-postgres/tests/test_store.py | 87 +++++++++++++++++++
2 files changed, 118 insertions(+), 12 deletions(-)
diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/base.py b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
index 2a908c90e..94c7d3e4e 100644
--- a/libs/checkpoint-postgres/langgraph/store/postgres/base.py
+++ b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
@@ -321,7 +321,7 @@ class BasePostgresStore(Generic[C]):
if op.query and self.index_config:
embedding_requests.append((idx, op.query))
- score_operator = _get_distance_operator(self)
+ score_operator, post_operator = _get_distance_operator(self)
vector_type = (
cast(PostgresIndexConfig, self.index_config)
.get("ann_index_config", {})
@@ -351,18 +351,28 @@ class BasePostgresStore(Generic[C]):
if not filter_conditions
else " AND " + " AND ".join(filter_conditions)
)
+ if op.namespace_prefix:
+ prefix_filter_str = f"WHERE s.prefix LIKE %s {filter_str} "
+ ns_args: Sequence = (f"{_namespace_to_text(op.namespace_prefix)}%",)
+ else:
+ ns_args = ()
+ if filter_str:
+ prefix_filter_str = f"WHERE {filter_str} "
+ else:
+ prefix_filter_str = ""
+
base_query = f"""
WITH scored AS (
- SELECT s.prefix, s.key, s.value, s.created_at, s.updated_at, {score_operator} AS score
+ SELECT s.prefix, s.key, s.value, s.created_at, s.updated_at, {score_operator} AS neg_score
FROM store s
JOIN store_vectors sv ON s.prefix = sv.prefix AND s.key = sv.key
- WHERE s.prefix LIKE %s {filter_str}
- ORDER BY {score_operator} DESC
+ {prefix_filter_str}
+ ORDER BY {score_operator} ASC
LIMIT %s
)
SELECT * FROM (
SELECT DISTINCT ON (prefix, key)
- prefix, key, value, created_at, updated_at, score
+ prefix, key, value, created_at, updated_at, {post_operator} as score
FROM scored
ORDER BY prefix, key, score DESC
) AS unique_docs
@@ -372,7 +382,7 @@ class BasePostgresStore(Generic[C]):
"""
params = [
_PLACEHOLDER, # Vector placeholder
- f"{_namespace_to_text(op.namespace_prefix)}%",
+ *ns_args,
*filter_params,
_PLACEHOLDER,
expanded_limit,
@@ -702,7 +712,6 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
_paramslist[i] = embedding
for (idx, _), (query, params) in zip(search_ops, queries):
- # Execute the actual query
cur.execute(query, params)
rows = cast(list[Row], cur.fetchall())
results[idx] = [
@@ -915,7 +924,7 @@ def _decode_ns_bytes(namespace: Union[str, bytes, list]) -> tuple[str, ...]:
return tuple(namespace.split("."))
-def _get_distance_operator(store: Any) -> str:
+def _get_distance_operator(store: Any) -> tuple[str, str]:
"""Get the distance operator and score expression based on config."""
# Note: Today, we are not using ANN indices due to restrictions
# on PGVector's support for mixing vector and non-vector filters
@@ -936,12 +945,22 @@ def _get_distance_operator(store: Any) -> str:
config = cast(PostgresIndexConfig, store.index_config)
distance_type = config.get("distance_type", "cosine")
+ # Return the operator and the score expression
+ # The operator is used in the CTE and will be compatible with an ASCENDING ORDER
+ # sort clause.
+ # The score expression is used in the final query and will be compatible with
+ # a DESCENDING ORDER sort clause and the user's expectations of what the similarity score
+ # should be.
if distance_type == "l2":
- return "1 - (sv.embedding <-> %s::%s)"
+ # Final: "-(sv.embedding <-> %s::%s)"
+ # We return the "l2 similarity" so that the sorting order is the same
+ return "sv.embedding <-> %s::%s", "-scored.neg_score"
elif distance_type == "inner_product":
- return "-(sv.embedding <#> %s::%s)"
- else: # cosine
- return "1 - (sv.embedding <=> %s::%s)"
+ # Final: "-(sv.embedding <#> %s::%s)"
+ return "sv.embedding <#> %s::%s", "-(scored.neg_score)"
+ else: # cosine similarity
+ # Final: "1 - (sv.embedding <=> %s::%s)"
+ return "sv.embedding <=> %s::%s", "1 - scored.neg_score"
def _ensure_index_config(
diff --git a/libs/checkpoint-postgres/tests/test_store.py b/libs/checkpoint-postgres/tests/test_store.py
index c9d220fe0..35dfa2150 100644
--- a/libs/checkpoint-postgres/tests/test_store.py
+++ b/libs/checkpoint-postgres/tests/test_store.py
@@ -634,6 +634,7 @@ def test_embed_with_path_operation_config(
distance_type: str,
) -> None:
"""Test operation-level field configuration for vector search."""
+
with _create_vector_store(
vector_type,
distance_type,
@@ -695,3 +696,89 @@ def test_embed_with_path_operation_config(
# assert len(results) == 3
# doc5_result = next(r for r in results if r.key == "doc5")
# assert doc5_result.score is None
+
+
+def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
+ """
+ Compute cosine similarity between a vector X and a matrix Y.
+ Lazy import numpy for efficiency.
+ """
+
+ similarities = []
+ for y in Y:
+ dot_product = sum(a * b for a, b in zip(X, y))
+ norm1 = sum(a * a for a in X) ** 0.5
+ norm2 = sum(a * a for a in y) ** 0.5
+ similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
+ similarities.append(similarity)
+
+ return similarities
+
+
+def _inner_product(X: list[float], Y: list[list[float]]) -> list[float]:
+ """
+ Compute inner product between a vector X and a matrix Y.
+ Lazy import numpy for efficiency.
+ """
+
+ similarities = []
+ for y in Y:
+ similarity = sum(a * b for a, b in zip(X, y))
+ similarities.append(similarity)
+
+ return similarities
+
+
+def _neg_l2_distance(X: list[float], Y: list[list[float]]) -> list[float]:
+ """
+ Compute l2 distance between a vector X and a matrix Y.
+ Lazy import numpy for efficiency.
+ """
+
+ similarities = []
+ for y in Y:
+ similarity = sum((a - b) ** 2 for a, b in zip(X, y)) ** 0.5
+ similarities.append(-similarity)
+
+ return similarities
+
+
+@pytest.mark.parametrize(
+ "vector_type,distance_type",
+ [
+ ("vector", "cosine"),
+ ("vector", "inner_product"),
+ ("halfvec", "l2"),
+ ],
+)
+@pytest.mark.parametrize("query", ["aaa", "bbb", "ccc", "abcd", "poisson"])
+def test_scores(
+ fake_embeddings: CharacterEmbeddings,
+ vector_type: str,
+ distance_type: str,
+ query: str,
+) -> None:
+ """Test operation-level field configuration for vector search."""
+ with _create_vector_store(
+ vector_type,
+ distance_type,
+ fake_embeddings,
+ text_fields=["key0"],
+ ) as store:
+ doc = {
+ "key0": "aaa",
+ }
+ store.put(("test",), "doc", doc, index=["key0", "key1"])
+
+ results = store.search((), query=query)
+ vec0 = fake_embeddings.embed_query(doc["key0"])
+ vec1 = fake_embeddings.embed_query(query)
+ if distance_type == "cosine":
+ similarities = _cosine_similarity(vec1, [vec0])
+ elif distance_type == "inner_product":
+ similarities = _inner_product(vec1, [vec0])
+ elif distance_type == "l2":
+ similarities = _neg_l2_distance(vec1, [vec0])
+
+ assert len(results) == 1
+ assert results[0].score == pytest.approx(similarities[0], abs=1e-3)
From fe538d4bcbecf2eecb8a0cc2ace548908d24e608 Mon Sep 17 00:00:00 2001
From: Mingqi Hu
Date: Tue, 3 Dec 2024 09:36:13 +0800
Subject: [PATCH 090/149] docs: Use edit mode as default to install template
(#2590)
Small change to install the dependencies with `edit` mode so that users
or freshman can see the effect immediately when they change the template
code. As below,
`pip install -e .`
It's very good to evaluate how agent works and easy to test &
re-develop!
---------
Signed-off-by: Mingqi Hu
Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
---
docs/docs/tutorials/langgraph-platform/local-server.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/docs/tutorials/langgraph-platform/local-server.md b/docs/docs/tutorials/langgraph-platform/local-server.md
index 9c8e3ea9f..db7f53fea 100644
--- a/docs/docs/tutorials/langgraph-platform/local-server.md
+++ b/docs/docs/tutorials/langgraph-platform/local-server.md
@@ -35,10 +35,10 @@ Create a new app from the `react-agent` template. This template is a simple agen
## Install Dependencies
-In the root of your new LangGraph app, install the dependencies:
+In the root of your new LangGraph app, install the dependencies in `edit` mode so your local changes are used by the server:
```shell
-pip install .
+pip install -e .
```
## Create a `.env` file
From 15f0765d60f18a9c3a2015af3c6506232750643c Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Mon, 2 Dec 2024 17:42:29 -0800
Subject: [PATCH 091/149] Add IVFFlat and HNSW support (#2598)
It seems that actually once i moved the operators & other things out,
the query planner does do reasonable things and do sequential scanning
if filtered N < some size but the index otherwise, even with namespace
filtering.
---
.../langgraph/store/postgres/base.py | 70 ++++++++++++++++++-
1 file changed, 67 insertions(+), 3 deletions(-)
diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/base.py b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
index 94c7d3e4e..28edd8998 100644
--- a/libs/checkpoint-postgres/langgraph/store/postgres/base.py
+++ b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
@@ -56,6 +56,7 @@ class Migration(NamedTuple):
sql: str
params: Optional[dict[str, Any]] = None
+ condition: Optional[Callable[["BasePostgresStore"], bool]] = None
MIGRATIONS: Sequence[str] = [
@@ -104,11 +105,29 @@ CREATE TABLE IF NOT EXISTS store_vectors (
),
},
),
- # TODO: Add an HNSW or IVFFlat index depending on config
- # First must improve the search query when filtering by
- # namespace
+ Migration(
+ """
+CREATE INDEX IF NOT EXISTS store_vectors_embedding_idx ON store_vectors
+ USING %(index_type)s (embedding %(ops)s)%(index_params)s;
+""",
+ condition=lambda store: bool(
+ store.index_config and _get_index_params(store)[0] != "flat"
+ ),
+ params={
+ "index_type": lambda store: _get_index_params(store)[0],
+ "ops": lambda store: _get_vector_type_ops(store),
+ "index_params": lambda store: (
+ " WITH ("
+ + ", ".join(f"{k}={v}" for k, v in _get_index_params(store)[1].items())
+ + ")"
+ if _get_index_params(store)[1]
+ else ""
+ ),
+ },
+ ),
]
+
C = TypeVar("C", bound=Union[_pg_internal.Conn, _ainternal.Conn])
@@ -140,6 +159,8 @@ class PoolConfig(TypedDict, total=False):
class ANNIndexConfig(TypedDict, total=False):
"""Configuration for vector index in PostgreSQL store."""
+ kind: Literal["hnsw", "ivfflat", "flat"]
+ """Type of index to use: 'hnsw' for Hierarchical Navigable Small World, or 'ivfflat' for Inverted File Flat."""
vector_type: Literal["vector", "halfvec"]
"""Type of vector storage to use.
Options:
@@ -148,6 +169,35 @@ class ANNIndexConfig(TypedDict, total=False):
"""
+class HNSWConfig(ANNIndexConfig, total=False):
+ """Configuration for HNSW (Hierarchical Navigable Small World) index."""
+
+ kind: Literal["hnsw"] # type: ignore[misc]
+ m: int
+ """Maximum number of connections per layer. Default is 16."""
+ ef_construction: int
+ """Size of dynamic candidate list for index construction. Default is 64."""
+
+
+class IVFFlatConfig(ANNIndexConfig, total=False):
+ """IVFFlat index divides vectors into lists, and then searches a subset of those lists that are closest to the query vector. It has faster build times and uses less memory than HNSW, but has lower query performance (in terms of speed-recall tradeoff).
+
+ Three keys to achieving good recall are:
+ 1. Create the index after the table has some data
+ 2. Choose an appropriate number of lists - a good place to start is rows / 1000 for up to 1M rows and sqrt(rows) for over 1M rows
+ 3. When querying, specify an appropriate number of probes (higher is better for recall, lower is better for speed) - a good place to start is sqrt(lists)
+ """
+
+ kind: Literal["ivfflat"] # type: ignore[misc]
+ nlist: int
+ """Number of inverted lists (clusters) for IVF index.
+
+ Determines the number of clusters used in the index structure.
+ Higher values can improve search speed but increase index size and build time.
+ Typically set to the square root of the number of vectors in the index.
+ """
+
+
class PostgresIndexConfig(IndexConfig, total=False):
"""Configuration for vector embeddings in PostgreSQL store with pgvector-specific options.
@@ -774,6 +824,8 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
for v, migration in enumerate(
self.VECTOR_MIGRATIONS[version + 1 :], start=version + 1
):
+ if migration.condition and not migration.condition(self):
+ continue
sql = migration.sql
if migration.params:
params = {
@@ -832,6 +884,18 @@ def _get_vector_type_ops(store: BasePostgresStore) -> str:
return f"{type_prefix}_{distance_suffix}"
+def _get_index_params(store: Any) -> tuple[str, dict[str, Any]]:
+ """Get the index type and configuration based on config."""
+ if not store.index_config:
+ return "hnsw", {}
+
+ config = cast(PostgresIndexConfig, store.index_config)
+ index_config = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
+ kind = index_config.pop("kind", "hnsw")
+ index_config.pop("vector_type", None)
+ return kind, index_config
+
+
def _namespace_to_text(
namespace: tuple[str, ...], handle_wildcards: bool = False
) -> str:
From 4332a9515d1aba33049d68a468b9a110e4e850bf Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Mon, 2 Dec 2024 17:55:26 -0800
Subject: [PATCH 092/149] Fixup initial provisioning of aio postgres db (#2571)
(#2600)
fixes #2570
---------
Co-authored-by: Tai Groot
---
.../langgraph/checkpoint/postgres/__init__.py | 18 +-
.../langgraph/checkpoint/postgres/aio.py | 19 +-
libs/checkpoint-postgres/tests/conftest.py | 1 +
libs/checkpoint-postgres/tests/test_async.py | 289 ++++++++++++------
libs/checkpoint-postgres/tests/test_sync.py | 275 +++++++++++------
5 files changed, 408 insertions(+), 194 deletions(-)
diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py
index d8af3aeca..e5a3cce55 100644
--- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py
+++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py
@@ -5,7 +5,6 @@ from typing import Any, Optional
from langchain_core.runnables import RunnableConfig
from psycopg import Capabilities, Connection, Cursor, Pipeline
-from psycopg.errors import UndefinedTable
from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import ConnectionPool
@@ -76,16 +75,15 @@ class PostgresSaver(BasePostgresSaver):
the first time checkpointer is used.
"""
with self._cursor() as cur:
- try:
- row = cur.execute(
- "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
- ).fetchone()
- if row is None:
- version = -1
- else:
- version = row["v"]
- except UndefinedTable:
+ cur.execute(self.MIGRATIONS[0])
+ results = cur.execute(
+ "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
+ )
+ row = results.fetchone()
+ if row is None:
version = -1
+ else:
+ version = row["v"]
for v, migration in zip(
range(version + 1, len(self.MIGRATIONS)),
self.MIGRATIONS[version + 1 :],
diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
index 440cb452e..4c0f5295c 100644
--- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
+++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
@@ -5,7 +5,6 @@ from typing import Any, Optional
from langchain_core.runnables import RunnableConfig
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
-from psycopg.errors import UndefinedTable
from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import AsyncConnectionPool
@@ -81,17 +80,15 @@ class AsyncPostgresSaver(BasePostgresSaver):
the first time checkpointer is used.
"""
async with self._cursor() as cur:
- try:
- results = await cur.execute(
- "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
- )
- row = await results.fetchone()
- if row is None:
- version = -1
- else:
- version = row["v"]
- except UndefinedTable:
+ await cur.execute(self.MIGRATIONS[0])
+ results = await cur.execute(
+ "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
+ )
+ row = await results.fetchone()
+ if row is None:
version = -1
+ else:
+ version = row["v"]
for v, migration in zip(
range(version + 1, len(self.MIGRATIONS)),
self.MIGRATIONS[version + 1 :],
diff --git a/libs/checkpoint-postgres/tests/conftest.py b/libs/checkpoint-postgres/tests/conftest.py
index ab59dbc6b..b44977ebd 100644
--- a/libs/checkpoint-postgres/tests/conftest.py
+++ b/libs/checkpoint-postgres/tests/conftest.py
@@ -7,6 +7,7 @@ from psycopg.rows import DictRow, dict_row
from tests.embed_test_utils import CharacterEmbeddings
+DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5441/"
DEFAULT_URI = "postgres://postgres:postgres@localhost:5441/postgres?sslmode=disable"
diff --git a/libs/checkpoint-postgres/tests/test_async.py b/libs/checkpoint-postgres/tests/test_async.py
index 73c376fd2..d4d0eb8fa 100644
--- a/libs/checkpoint-postgres/tests/test_async.py
+++ b/libs/checkpoint-postgres/tests/test_async.py
@@ -1,7 +1,14 @@
+# type: ignore
+
+from contextlib import asynccontextmanager
from typing import Any
+from uuid import uuid4
import pytest
from langchain_core.runnables import RunnableConfig
+from psycopg import AsyncConnection
+from psycopg.rows import dict_row
+from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.base import (
Checkpoint,
@@ -10,104 +17,212 @@ from langgraph.checkpoint.base import (
empty_checkpoint,
)
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
-from tests.conftest import DEFAULT_URI
+from tests.conftest import DEFAULT_POSTGRES_URI
-class TestAsyncPostgresSaver:
- @pytest.fixture(autouse=True)
- async def setup(self) -> None:
- # objects for test setup
- self.config_1: RunnableConfig = {
- "configurable": {
- "thread_id": "thread-1",
- # for backwards compatibility testing
- "thread_ts": "1",
- "checkpoint_ns": "",
- }
- }
- self.config_2: RunnableConfig = {
- "configurable": {
- "thread_id": "thread-2",
- "checkpoint_id": "2",
- "checkpoint_ns": "",
- }
- }
- self.config_3: RunnableConfig = {
- "configurable": {
- "thread_id": "thread-2",
- "checkpoint_id": "2-inner",
- "checkpoint_ns": "inner",
- }
- }
+@asynccontextmanager
+async def _pool_saver():
+ """Fixture for pool mode testing."""
+ 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 AsyncConnectionPool(
+ DEFAULT_POSTGRES_URI + database,
+ max_size=10,
+ kwargs={"autocommit": True, "row_factory": dict_row},
+ ) as pool:
+ checkpointer = AsyncPostgresSaver(pool)
+ 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}")
- self.chkpnt_1: Checkpoint = empty_checkpoint()
- self.chkpnt_2: Checkpoint = create_checkpoint(self.chkpnt_1, {}, 1)
- self.chkpnt_3: Checkpoint = empty_checkpoint()
- self.metadata_1: CheckpointMetadata = {
- "source": "input",
- "step": 2,
- "writes": {},
- "score": 1,
+@asynccontextmanager
+async def _pipe_saver():
+ """Fixture for pipeline mode testing."""
+ 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:
+ async with await AsyncConnection.connect(
+ DEFAULT_POSTGRES_URI + database,
+ autocommit=True,
+ prepare_threshold=0,
+ row_factory=dict_row,
+ ) as conn:
+ async with conn.pipeline() as pipe:
+ checkpointer = AsyncPostgresSaver(conn, pipe=pipe)
+ await checkpointer.setup()
+ async with conn.pipeline() as pipe:
+ checkpointer = AsyncPostgresSaver(conn, 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}")
+
+
+@asynccontextmanager
+async def _base_saver():
+ """Fixture for regular connection mode testing."""
+ 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:
+ async with await AsyncConnection.connect(
+ DEFAULT_POSTGRES_URI + database,
+ autocommit=True,
+ prepare_threshold=0,
+ row_factory=dict_row,
+ ) as conn:
+ checkpointer = AsyncPostgresSaver(conn)
+ 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}")
+
+
+@asynccontextmanager
+async def _saver(name: str):
+ if name == "base":
+ async with _base_saver() as saver:
+ yield saver
+ elif name == "pool":
+ async with _pool_saver() as saver:
+ yield saver
+ elif name == "pipe":
+ async with _pipe_saver() as saver:
+ yield saver
+
+
+@pytest.fixture
+def test_data():
+ """Fixture providing test data for checkpoint tests."""
+ config_1: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-1",
+ # for backwards compatibility testing
+ "thread_ts": "1",
+ "checkpoint_ns": "",
}
- self.metadata_2: CheckpointMetadata = {
- "source": "loop",
+ }
+ config_2: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-2",
+ "checkpoint_id": "2",
+ "checkpoint_ns": "",
+ }
+ }
+ config_3: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-2",
+ "checkpoint_id": "2-inner",
+ "checkpoint_ns": "inner",
+ }
+ }
+
+ chkpnt_1: Checkpoint = empty_checkpoint()
+ chkpnt_2: Checkpoint = create_checkpoint(chkpnt_1, {}, 1)
+ chkpnt_3: Checkpoint = empty_checkpoint()
+
+ metadata_1: CheckpointMetadata = {
+ "source": "input",
+ "step": 2,
+ "writes": {},
+ "score": 1,
+ }
+ metadata_2: CheckpointMetadata = {
+ "source": "loop",
+ "step": 1,
+ "writes": {"foo": "bar"},
+ "score": None,
+ }
+ metadata_3: CheckpointMetadata = {}
+
+ return {
+ "configs": [config_1, config_2, config_3],
+ "checkpoints": [chkpnt_1, chkpnt_2, chkpnt_3],
+ "metadata": [metadata_1, metadata_2, metadata_3],
+ }
+
+
+@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
+async def test_asearch(request, saver_name: str, test_data) -> None:
+ async with _saver(saver_name) as saver:
+ configs = test_data["configs"]
+ checkpoints = test_data["checkpoints"]
+ metadata = test_data["metadata"]
+
+ await saver.aput(configs[0], checkpoints[0], metadata[0], {})
+ await saver.aput(configs[1], checkpoints[1], metadata[1], {})
+ await saver.aput(configs[2], checkpoints[2], metadata[2], {})
+
+ # call method / assertions
+ query_1 = {"source": "input"} # search by 1 key
+ query_2 = {
"step": 1,
"writes": {"foo": "bar"},
- "score": None,
- }
- self.metadata_3: CheckpointMetadata = {}
- async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
- await saver.setup()
+ } # search by multiple keys
+ query_3: dict[str, Any] = {} # search by no keys, return all checkpoints
+ query_4 = {"source": "update", "step": 1} # no match
- async def test_asearch(self) -> None:
- async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
- await saver.aput(self.config_1, self.chkpnt_1, self.metadata_1, {})
- await saver.aput(self.config_2, self.chkpnt_2, self.metadata_2, {})
- await saver.aput(self.config_3, self.chkpnt_3, self.metadata_3, {})
+ search_results_1 = [c async for c in saver.alist(None, filter=query_1)]
+ assert len(search_results_1) == 1
+ assert search_results_1[0].metadata == metadata[0]
- # call method / assertions
- query_1 = {"source": "input"} # search by 1 key
- query_2 = {
- "step": 1,
- "writes": {"foo": "bar"},
- } # search by multiple keys
- query_3: dict[str, Any] = {} # search by no keys, return all checkpoints
- query_4 = {"source": "update", "step": 1} # no match
+ search_results_2 = [c async for c in saver.alist(None, filter=query_2)]
+ assert len(search_results_2) == 1
+ assert search_results_2[0].metadata == metadata[1]
- search_results_1 = [c async for c in saver.alist(None, filter=query_1)]
- assert len(search_results_1) == 1
- assert search_results_1[0].metadata == self.metadata_1
+ search_results_3 = [c async for c in saver.alist(None, filter=query_3)]
+ assert len(search_results_3) == 3
- search_results_2 = [c async for c in saver.alist(None, filter=query_2)]
- assert len(search_results_2) == 1
- assert search_results_2[0].metadata == self.metadata_2
+ search_results_4 = [c async for c in saver.alist(None, filter=query_4)]
+ assert len(search_results_4) == 0
- search_results_3 = [c async for c in saver.alist(None, filter=query_3)]
- assert len(search_results_3) == 3
+ # search by config (defaults to checkpoints across all namespaces)
+ search_results_5 = [
+ c async for c in saver.alist({"configurable": {"thread_id": "thread-2"}})
+ ]
+ assert len(search_results_5) == 2
+ assert {
+ search_results_5[0].config["configurable"]["checkpoint_ns"],
+ search_results_5[1].config["configurable"]["checkpoint_ns"],
+ } == {"", "inner"}
- search_results_4 = [c async for c in saver.alist(None, filter=query_4)]
- assert len(search_results_4) == 0
- # search by config (defaults to checkpoints across all namespaces)
- search_results_5 = [
- c
- async for c in saver.alist({"configurable": {"thread_id": "thread-2"}})
- ]
- assert len(search_results_5) == 2
- assert {
- search_results_5[0].config["configurable"]["checkpoint_ns"],
- search_results_5[1].config["configurable"]["checkpoint_ns"],
- } == {"", "inner"}
-
- # TODO: test before and limit params
-
- async def test_null_chars(self) -> None:
- async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
- config = await saver.aput(
- self.config_1, self.chkpnt_1, {"my_key": "\x00abc"}, {}
- )
- assert (await saver.aget_tuple(config)).metadata["my_key"] == "abc" # type: ignore
- assert [c async for c in saver.alist(None, filter={"my_key": "abc"})][
- 0
- ].metadata["my_key"] == "abc"
+@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
+async def test_null_chars(request, saver_name: str, test_data) -> None:
+ async with _saver(saver_name) as saver:
+ config = await saver.aput(
+ test_data["configs"][0],
+ test_data["checkpoints"][0],
+ {"my_key": "\x00abc"},
+ {},
+ )
+ assert (await saver.aget_tuple(config)).metadata["my_key"] == "abc" # type: ignore
+ assert [c async for c in saver.alist(None, filter={"my_key": "abc"})][
+ 0
+ ].metadata["my_key"] == "abc"
diff --git a/libs/checkpoint-postgres/tests/test_sync.py b/libs/checkpoint-postgres/tests/test_sync.py
index 052e699b3..fbf4c1c88 100644
--- a/libs/checkpoint-postgres/tests/test_sync.py
+++ b/libs/checkpoint-postgres/tests/test_sync.py
@@ -1,7 +1,14 @@
+# type: ignore
+
+from contextlib import contextmanager
from typing import Any
+from uuid import uuid4
import pytest
from langchain_core.runnables import RunnableConfig
+from psycopg import Connection
+from psycopg.rows import dict_row
+from psycopg_pool import ConnectionPool
from langgraph.checkpoint.base import (
Checkpoint,
@@ -10,103 +17,199 @@ from langgraph.checkpoint.base import (
empty_checkpoint,
)
from langgraph.checkpoint.postgres import PostgresSaver
-from tests.conftest import DEFAULT_URI
+from tests.conftest import DEFAULT_POSTGRES_URI
-class TestPostgresSaver:
- @pytest.fixture(autouse=True)
- def setup(self) -> None:
- # objects for test setup
- self.config_1: RunnableConfig = {
- "configurable": {
- "thread_id": "thread-1",
- # for backwards compatibility testing
- "thread_ts": "1",
- "checkpoint_ns": "",
- }
- }
- self.config_2: RunnableConfig = {
- "configurable": {
- "thread_id": "thread-2",
- "checkpoint_id": "2",
- "checkpoint_ns": "",
- }
- }
- self.config_3: RunnableConfig = {
- "configurable": {
- "thread_id": "thread-2",
- "checkpoint_id": "2-inner",
- "checkpoint_ns": "inner",
- }
- }
+@contextmanager
+def _pool_saver():
+ """Fixture for pool mode testing."""
+ 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 ConnectionPool(
+ DEFAULT_POSTGRES_URI + database,
+ max_size=10,
+ kwargs={"autocommit": True, "row_factory": dict_row},
+ ) as pool:
+ checkpointer = PostgresSaver(pool)
+ checkpointer.setup()
+ yield checkpointer
+ finally:
+ # drop unique db
+ with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
+ conn.execute(f"DROP DATABASE {database}")
- self.chkpnt_1: Checkpoint = empty_checkpoint()
- self.chkpnt_2: Checkpoint = create_checkpoint(self.chkpnt_1, {}, 1)
- self.chkpnt_3: Checkpoint = empty_checkpoint()
- self.metadata_1: CheckpointMetadata = {
- "source": "input",
- "step": 2,
- "writes": {},
- "score": 1,
+@contextmanager
+def _pipe_saver():
+ """Fixture for pipeline mode testing."""
+ 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:
+ with Connection.connect(
+ DEFAULT_POSTGRES_URI + database,
+ autocommit=True,
+ prepare_threshold=0,
+ row_factory=dict_row,
+ ) as conn:
+ with conn.pipeline() as pipe:
+ checkpointer = PostgresSaver(conn, pipe=pipe)
+ checkpointer.setup()
+ with conn.pipeline() as pipe:
+ checkpointer = PostgresSaver(conn, pipe=pipe)
+ yield checkpointer
+ finally:
+ # drop unique db
+ with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
+ conn.execute(f"DROP DATABASE {database}")
+
+
+@contextmanager
+def _base_saver():
+ """Fixture for regular connection mode testing."""
+ 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:
+ with Connection.connect(
+ DEFAULT_POSTGRES_URI + database,
+ autocommit=True,
+ prepare_threshold=0,
+ row_factory=dict_row,
+ ) as conn:
+ checkpointer = PostgresSaver(conn)
+ checkpointer.setup()
+ yield checkpointer
+ finally:
+ # drop unique db
+ with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
+ conn.execute(f"DROP DATABASE {database}")
+
+
+@contextmanager
+def _saver(name: str):
+ if name == "base":
+ with _base_saver() as saver:
+ yield saver
+ elif name == "pool":
+ with _pool_saver() as saver:
+ yield saver
+ elif name == "pipe":
+ with _pipe_saver() as saver:
+ yield saver
+
+
+@pytest.fixture
+def test_data():
+ """Fixture providing test data for checkpoint tests."""
+ config_1: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-1",
+ # for backwards compatibility testing
+ "thread_ts": "1",
+ "checkpoint_ns": "",
}
- self.metadata_2: CheckpointMetadata = {
- "source": "loop",
+ }
+ config_2: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-2",
+ "checkpoint_id": "2",
+ "checkpoint_ns": "",
+ }
+ }
+ config_3: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-2",
+ "checkpoint_id": "2-inner",
+ "checkpoint_ns": "inner",
+ }
+ }
+
+ chkpnt_1: Checkpoint = empty_checkpoint()
+ chkpnt_2: Checkpoint = create_checkpoint(chkpnt_1, {}, 1)
+ chkpnt_3: Checkpoint = empty_checkpoint()
+
+ metadata_1: CheckpointMetadata = {
+ "source": "input",
+ "step": 2,
+ "writes": {},
+ "score": 1,
+ }
+ metadata_2: CheckpointMetadata = {
+ "source": "loop",
+ "step": 1,
+ "writes": {"foo": "bar"},
+ "score": None,
+ }
+ metadata_3: CheckpointMetadata = {}
+
+ return {
+ "configs": [config_1, config_2, config_3],
+ "checkpoints": [chkpnt_1, chkpnt_2, chkpnt_3],
+ "metadata": [metadata_1, metadata_2, metadata_3],
+ }
+
+
+@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
+def test_search(saver_name: str, test_data) -> None:
+ with _saver(saver_name) as saver:
+ configs = test_data["configs"]
+ checkpoints = test_data["checkpoints"]
+ metadata = test_data["metadata"]
+
+ saver.put(configs[0], checkpoints[0], metadata[0], {})
+ saver.put(configs[1], checkpoints[1], metadata[1], {})
+ saver.put(configs[2], checkpoints[2], metadata[2], {})
+
+ # call method / assertions
+ query_1 = {"source": "input"} # search by 1 key
+ query_2 = {
"step": 1,
"writes": {"foo": "bar"},
- "score": None,
- }
- self.metadata_3: CheckpointMetadata = {}
- with PostgresSaver.from_conn_string(DEFAULT_URI) as saver:
- saver.setup()
+ } # search by multiple keys
+ query_3: dict[str, Any] = {} # search by no keys, return all checkpoints
+ query_4 = {"source": "update", "step": 1} # no match
- def test_search(self) -> None:
- with PostgresSaver.from_conn_string(DEFAULT_URI) as saver:
- # save checkpoints
- saver.put(self.config_1, self.chkpnt_1, self.metadata_1, {})
- saver.put(self.config_2, self.chkpnt_2, self.metadata_2, {})
- saver.put(self.config_3, self.chkpnt_3, self.metadata_3, {})
+ search_results_1 = list(saver.list(None, filter=query_1))
+ assert len(search_results_1) == 1
+ assert search_results_1[0].metadata == metadata[0]
- # call method / assertions
- query_1 = {"source": "input"} # search by 1 key
- query_2 = {
- "step": 1,
- "writes": {"foo": "bar"},
- } # search by multiple keys
- query_3: dict[str, Any] = {} # search by no keys, return all checkpoints
- query_4 = {"source": "update", "step": 1} # no match
+ search_results_2 = list(saver.list(None, filter=query_2))
+ assert len(search_results_2) == 1
+ assert search_results_2[0].metadata == metadata[1]
- search_results_1 = list(saver.list(None, filter=query_1))
- assert len(search_results_1) == 1
- assert search_results_1[0].metadata == self.metadata_1
+ search_results_3 = list(saver.list(None, filter=query_3))
+ assert len(search_results_3) == 3
- search_results_2 = list(saver.list(None, filter=query_2))
- assert len(search_results_2) == 1
- assert search_results_2[0].metadata == self.metadata_2
+ search_results_4 = list(saver.list(None, filter=query_4))
+ assert len(search_results_4) == 0
- search_results_3 = list(saver.list(None, filter=query_3))
- assert len(search_results_3) == 3
+ # search by config (defaults to checkpoints across all namespaces)
+ search_results_5 = list(saver.list({"configurable": {"thread_id": "thread-2"}}))
+ assert len(search_results_5) == 2
+ assert {
+ search_results_5[0].config["configurable"]["checkpoint_ns"],
+ search_results_5[1].config["configurable"]["checkpoint_ns"],
+ } == {"", "inner"}
- search_results_4 = list(saver.list(None, filter=query_4))
- assert len(search_results_4) == 0
- # search by config (defaults to checkpoints across all namespaces)
- search_results_5 = list(
- saver.list({"configurable": {"thread_id": "thread-2"}})
- )
- assert len(search_results_5) == 2
- assert {
- search_results_5[0].config["configurable"]["checkpoint_ns"],
- search_results_5[1].config["configurable"]["checkpoint_ns"],
- } == {"", "inner"}
-
- # TODO: test before and limit params
-
- def test_null_chars(self) -> None:
- with PostgresSaver.from_conn_string(DEFAULT_URI) as saver:
- config = saver.put(self.config_1, self.chkpnt_1, {"my_key": "\x00abc"}, {})
- assert saver.get_tuple(config).metadata["my_key"] == "abc" # type: ignore
- assert (
- list(saver.list(None, filter={"my_key": "abc"}))[0].metadata["my_key"] # type: ignore
- == "abc"
- )
+@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
+def test_null_chars(saver_name: str, test_data) -> None:
+ with _saver(saver_name) as saver:
+ config = saver.put(
+ test_data["configs"][0],
+ test_data["checkpoints"][0],
+ {"my_key": "\x00abc"},
+ {},
+ )
+ assert saver.get_tuple(config).metadata["my_key"] == "abc" # type: ignore
+ assert (
+ list(saver.list(None, filter={"my_key": "abc"}))[0].metadata["my_key"]
+ == "abc"
+ )
From 0361554fcf8c1f7b5a58e179bd532416f220f1e5 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Mon, 2 Dec 2024 17:56:23 -0800
Subject: [PATCH 093/149] Bump Checkpoint Postgres (#2601)
---
libs/checkpoint-postgres/pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/checkpoint-postgres/pyproject.toml b/libs/checkpoint-postgres/pyproject.toml
index a121a1b39..f3af7d14d 100644
--- a/libs/checkpoint-postgres/pyproject.toml
+++ b/libs/checkpoint-postgres/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-postgres"
-version = "2.0.6"
+version = "2.0.7"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
From 36b6cd1493c01c8e6a32f611a1ec2c26c2e8f902 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Mon, 2 Dec 2024 18:20:19 -0800
Subject: [PATCH 094/149] fix: Handle empty store similarity (numpy) (#2602)
---
libs/checkpoint/langgraph/store/memory/__init__.py | 2 ++
libs/checkpoint/pyproject.toml | 2 +-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/libs/checkpoint/langgraph/store/memory/__init__.py b/libs/checkpoint/langgraph/store/memory/__init__.py
index 40011a7ba..eec511ac8 100644
--- a/libs/checkpoint/langgraph/store/memory/__init__.py
+++ b/libs/checkpoint/langgraph/store/memory/__init__.py
@@ -413,6 +413,8 @@ def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
Compute cosine similarity between a vector X and a matrix Y.
Lazy import numpy for efficiency.
"""
+ if not Y:
+ return []
if _check_numpy():
import numpy as np # type: ignore
diff --git a/libs/checkpoint/pyproject.toml b/libs/checkpoint/pyproject.toml
index ef12f2052..f48f46a1f 100644
--- a/libs/checkpoint/pyproject.toml
+++ b/libs/checkpoint/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
-version = "2.0.7"
+version = "2.0.8"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
From 3bf92d0b031e7f2a54cf51d28d51ebcb3f04cf2e Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Tue, 3 Dec 2024 11:04:28 -0800
Subject: [PATCH 095/149] Fix
---
libs/sdk-py/langgraph_sdk/client.py | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py
index 3fbf61041..d6c5b6f05 100644
--- a/libs/sdk-py/langgraph_sdk/client.py
+++ b/libs/sdk-py/langgraph_sdk/client.py
@@ -298,9 +298,7 @@ class HttpClient:
logger.error(f"Error from langgraph-api: {body}", exc_info=e)
raise e
# check content type
- content_type = self._response.headers.get("content-type", "").partition(
- ";"
- )[0]
+ content_type = res.headers.get("content-type", "").partition(";")[0]
if "text/event-stream" not in content_type:
raise httpx.TransportError(
"Expected response header Content-Type to contain 'text/event-stream', "
@@ -2447,9 +2445,7 @@ class SyncHttpClient:
logger.error(f"Error from langgraph-api: {body}", exc_info=e)
raise e
# check content type
- content_type = self._response.headers.get("content-type", "").partition(
- ";"
- )[0]
+ content_type = res.headers.get("content-type", "").partition(";")[0]
if "text/event-stream" not in content_type:
raise httpx.TransportError(
"Expected response header Content-Type to contain 'text/event-stream', "
From 515242d0bac763cded351272437ed7db130de719 Mon Sep 17 00:00:00 2001
From: vbarda
Date: Tue, 3 Dec 2024 15:40:18 -0500
Subject: [PATCH 096/149] langgraph: allow passing kwargs to SDK methods in
RemoteGraph's invoke/stream
---
libs/langgraph/langgraph/pregel/remote.py | 20 ++++++++++++--------
1 file changed, 12 insertions(+), 8 deletions(-)
diff --git a/libs/langgraph/langgraph/pregel/remote.py b/libs/langgraph/langgraph/pregel/remote.py
index d45cdb310..45b837dd7 100644
--- a/libs/langgraph/langgraph/pregel/remote.py
+++ b/libs/langgraph/langgraph/pregel/remote.py
@@ -575,6 +575,7 @@ class RemoteGraph(PregelProtocol):
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
subgraphs: bool = False,
+ **kwargs: Any,
) -> Iterator[Union[dict[str, Any], Any]]:
"""Create a run and stream the results.
@@ -589,6 +590,7 @@ class RemoteGraph(PregelProtocol):
interrupt_before: Interrupt the graph before these nodes.
interrupt_after: Interrupt the graph after these nodes.
subgraphs: Stream from subgraphs.
+ **kwargs: Additional params to pass to client.runs.stream.
Yields:
The output of the graph.
@@ -616,6 +618,7 @@ class RemoteGraph(PregelProtocol):
interrupt_after=interrupt_after,
stream_subgraphs=subgraphs or stream is not None,
if_not_exists="create",
+ **kwargs,
):
# split mode and ns
if NS_SEP in chunk.event:
@@ -664,6 +667,7 @@ class RemoteGraph(PregelProtocol):
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
subgraphs: bool = False,
+ **kwargs: Any,
) -> AsyncIterator[Union[dict[str, Any], Any]]:
"""Create a run and stream the results.
@@ -678,6 +682,7 @@ class RemoteGraph(PregelProtocol):
interrupt_before: Interrupt the graph before these nodes.
interrupt_after: Interrupt the graph after these nodes.
subgraphs: Stream from subgraphs.
+ **kwargs: Additional params to pass to client.runs.stream.
Yields:
The output of the graph.
@@ -705,6 +710,7 @@ class RemoteGraph(PregelProtocol):
interrupt_after=interrupt_after,
stream_subgraphs=subgraphs or stream is not None,
if_not_exists="create",
+ **kwargs,
):
# split mode and ns
if NS_SEP in chunk.event:
@@ -767,18 +773,16 @@ class RemoteGraph(PregelProtocol):
*,
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
+ **kwargs: Any,
) -> Union[dict[str, Any], Any]:
"""Create a run, wait until it finishes and return the final state.
- This method calls `POST /threads/{thread_id}/runs/wait` if a `thread_id`
- is speciffed in the `configurable` field of the config or
- `POST /runs/wait` otherwise.
-
Args:
input: Input to the graph.
config: A `RunnableConfig` for graph invocation.
interrupt_before: Interrupt the graph before these nodes.
interrupt_after: Interrupt the graph after these nodes.
+ **kwargs: Additional params to pass to RemoteGraph.stream.
Returns:
The output of the graph.
@@ -789,6 +793,7 @@ class RemoteGraph(PregelProtocol):
interrupt_before=interrupt_before,
interrupt_after=interrupt_after,
stream_mode="values",
+ **kwargs,
):
pass
try:
@@ -803,18 +808,16 @@ class RemoteGraph(PregelProtocol):
*,
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
+ **kwargs: Any,
) -> Union[dict[str, Any], Any]:
"""Create a run, wait until it finishes and return the final state.
- This method calls `POST /threads/{thread_id}/runs/wait` if a `thread_id`
- is speciffed in the `configurable` field of the config or
- `POST /runs/wait` otherwise.
-
Args:
input: Input to the graph.
config: A `RunnableConfig` for graph invocation.
interrupt_before: Interrupt the graph before these nodes.
interrupt_after: Interrupt the graph after these nodes.
+ **kwargs: Additional params to pass to RemoteGraph.astream.
Returns:
The output of the graph.
@@ -825,6 +828,7 @@ class RemoteGraph(PregelProtocol):
interrupt_before=interrupt_before,
interrupt_after=interrupt_after,
stream_mode="values",
+ **kwargs,
):
pass
try:
From 9d755f54e4609121720c79de51520e069a081f67 Mon Sep 17 00:00:00 2001
From: bracesproul
Date: Wed, 9 Oct 2024 18:15:00 -0700
Subject: [PATCH 097/149] fix(docs): Small nits & typo fixes
---
CONTRIBUTING.md | 2 +-
docs/docs/cloud/how-tos/configuration_cloud.md | 4 ++--
docs/docs/concepts/human_in_the_loop.md | 4 ++--
docs/docs/concepts/persistence.md | 14 +++++++-------
4 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d7d723f0e..f311b3796 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -49,7 +49,7 @@ gain understanding of concepts and how they interact by showing one way to achie
They should **avoid** giving
multiple permutations of ways to achieve that goal in-depth. Choice is burdensome. Instead, they should guide a new user through a recommended path to accomplishing a concrete goal. While the end result of a tutorial does not necessarily need to
-be completely production-ready, it should be useful and practically satisfy the the goal that you clearly stated in the tutorial's introduction.
+be completely production-ready, it should be useful and practically satisfy the goal that you clearly stated in the tutorial's introduction.
To quote the Diataxis website:
diff --git a/docs/docs/cloud/how-tos/configuration_cloud.md b/docs/docs/cloud/how-tos/configuration_cloud.md
index 9b2f2091d..8954d966f 100644
--- a/docs/docs/cloud/how-tos/configuration_cloud.md
+++ b/docs/docs/cloud/how-tos/configuration_cloud.md
@@ -83,7 +83,7 @@ We can now call `.get_schemas` to get schemas associated with this graph:
assistant_id=assistant["assistant_id"]
)
# There are multiple types of schemas
- # We can get the `config_schema` to look at the the configurable parameters
+ # We can get the `config_schema` to look at the configurable parameters
print(schemas["config_schema"])
```
@@ -94,7 +94,7 @@ We can now call `.get_schemas` to get schemas associated with this graph:
assistant["assistant_id"]
);
// There are multiple types of schemas
- // We can get the `config_schema` to look at the the configurable parameters
+ // We can get the `config_schema` to look at the configurable parameters
console.log(schemas.config_schema);
```
diff --git a/docs/docs/concepts/human_in_the_loop.md b/docs/docs/concepts/human_in_the_loop.md
index f253c5ddb..0bb29a2c0 100644
--- a/docs/docs/concepts/human_in_the_loop.md
+++ b/docs/docs/concepts/human_in_the_loop.md
@@ -120,7 +120,7 @@ See [our guide](../how-tos/human_in_the_loop/breakpoints.ipynb) for a detailed h
Sometimes we want to review and edit the agent's state.
-As with approval, we can interrupt our agent at a [breakpoint](./low_level.md#breakpoints) prior the the step we want to check.
+As with approval, we can interrupt our agent at a [breakpoint](./low_level.md#breakpoints) prior the step we want to check.
We can surface the current state to a user and allow the user to edit the agent state.
@@ -170,7 +170,7 @@ With editing, the user makes a decision about whether or not to edit the graph s
With input, we explicitly define a node in our graph for collecting human input!
-The the state update with the human input then runs *as this node*.
+The state update with the human input then runs *as this node*.
```python
# Compile our graph with a checkpoitner and a breakpoint before the step to to collect human input
diff --git a/docs/docs/concepts/persistence.md b/docs/docs/concepts/persistence.md
index d5ccd6d15..e31296b9b 100644
--- a/docs/docs/concepts/persistence.md
+++ b/docs/docs/concepts/persistence.md
@@ -224,7 +224,7 @@ A [state schema](low_level.md#schema) specifies a set of keys that are populated
But, what if we want to retrain some information *across threads*? Consider the case of a chatbot where we want to retain specific information about the user across *all* chat conversations (e.g., threads) with that user!
-With checkpointers alone, we cannot share information across threads. This motivates the need for the `Store` interface. As an illustration, we can define an `InMemoryStore` to store information about a user across threads. We simply compile our graph with a checkpointer, as before, and will our new `in_memory_store`.
+With checkpointers alone, we cannot share information across threads. This motivates the need for the `Store` interface. As an illustration, we can define an `InMemoryStore` to store information about a user across threads. We simply compile our graph with a checkpointer, as before, and with our new `in_memory_store` variable.
First, let's showcase this in isolation without using LangGraph.
```python
@@ -239,7 +239,7 @@ user_id = "1"
namespace_for_memory = (user_id, "memories")
```
-We use the `store.put` to save memories to our namespace in the store. When we do this, we specify the namespace, as defined above, and a key-value pair for the memory: the key is simply a unique identifier for the memory (`memory_id`) and the value (a dictionary) is the memory itself.
+We use the `store.put` method to save memories to our namespace in the store. When we do this, we specify the namespace, as defined above, and a key-value pair for the memory: the key is simply a unique identifier for the memory (`memory_id`) and the value (a dictionary) is the memory itself.
```python
memory_id = str(uuid.uuid4())
@@ -247,7 +247,7 @@ memory = {"food_preference" : "I like pizza"}
in_memory_store.put(namespace_for_memory, memory_id, memory)
```
-We can read out memories in our namespace using `store.search`, which will return all memories for a given user as a list. The most recent memory is the last in the list.
+We can read out memories in our namespace using the `store.search` method, which will return all memories for a given user as a list. The most recent memory is the last in the list.
```python
memories = in_memory_store.search(namespace_for_memory)
@@ -259,16 +259,16 @@ memories[-1].dict()
'updated_at': '2024-10-02T17:22:31.590605+00:00'}
```
-Each memory type is a Python class with certain attributes. We can access it as a dictionary by converting via `.dict` as above.
+Each memory type is a Python class ([`Item`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/python_sdk_ref/?h=item#langgraph_sdk.schema.Item)) with certain attributes. We can access it as a dictionary by converting via `.dict` as above.
The attributes it has are:
- `value`: The value (itself a dictionary) of this memory
-- `key`: The UUID for this memory in this namespace
+- `key`: A unique key for this memory in this namespace
- `namespace`: A list of strings, the namespace of this memory type
- `created_at`: Timestamp for when this memory was created
- `updated_at`: Timestamp for when this memory was updated
-With this all in place, we use the `in_memory_store` in LangGraph. The `in_memory_store` works hand-in-hand with the checkpointer: the checkpointer saves state to threads, as discussed above, and the the `in_memory_store` allows us to store arbitrary information for access *across* threads. We compile the graph with both the checkpointer and the `in_memory_store` as follows.
+With this all in place, we use the `in_memory_store` in LangGraph. The `in_memory_store` works hand-in-hand with the checkpointer: the checkpointer saves state to threads, as discussed above, and the `in_memory_store` allows us to store arbitrary information for access *across* threads. We compile the graph with both the checkpointer and the `in_memory_store` as follows.
```python
from langgraph.checkpoint.memory import MemorySaver
@@ -317,7 +317,7 @@ def update_memory(state: MessagesState, config: RunnableConfig, *, store: BaseSt
```
-As we showed above, we can also access the store in any node and use `search` to get memories. Recall the the memories are returned as a list of objects that can be converted to a dictionary.
+As we showed above, we can also access the store in any node and use the `store.search` method to get memories. Recall the the memories are returned as a list of objects that can be converted to a dictionary.
```python
memories[-1].dict()
From 23d51629459f643414845f877d9b0b5f155450dd Mon Sep 17 00:00:00 2001
From: Brace Sproul
Date: Mon, 14 Oct 2024 08:01:46 -0700
Subject: [PATCH 098/149] Update human_in_the_loop.md
Co-authored-by: Vadym Barda
---
docs/docs/concepts/human_in_the_loop.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/docs/concepts/human_in_the_loop.md b/docs/docs/concepts/human_in_the_loop.md
index 0bb29a2c0..8728d4ba5 100644
--- a/docs/docs/concepts/human_in_the_loop.md
+++ b/docs/docs/concepts/human_in_the_loop.md
@@ -120,7 +120,7 @@ See [our guide](../how-tos/human_in_the_loop/breakpoints.ipynb) for a detailed h
Sometimes we want to review and edit the agent's state.
-As with approval, we can interrupt our agent at a [breakpoint](./low_level.md#breakpoints) prior the step we want to check.
+As with approval, we can interrupt our agent at a [breakpoint](./low_level.md#breakpoints) prior to the step we want to check.
We can surface the current state to a user and allow the user to edit the agent state.
From 70f323779e674e5ac8306502a4386692bd687908 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Tue, 3 Dec 2024 13:26:12 -0800
Subject: [PATCH 099/149] Update persistence.md
---
docs/docs/concepts/persistence.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/docs/concepts/persistence.md b/docs/docs/concepts/persistence.md
index e31296b9b..c6ad10a12 100644
--- a/docs/docs/concepts/persistence.md
+++ b/docs/docs/concepts/persistence.md
@@ -259,7 +259,7 @@ memories[-1].dict()
'updated_at': '2024-10-02T17:22:31.590605+00:00'}
```
-Each memory type is a Python class ([`Item`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/python_sdk_ref/?h=item#langgraph_sdk.schema.Item)) with certain attributes. We can access it as a dictionary by converting via `.dict` as above.
+Each memory type is a Python class ([`Item`](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.Item)) with certain attributes. We can access it as a dictionary by converting via `.dict` as above.
The attributes it has are:
- `value`: The value (itself a dictionary) of this memory
@@ -405,4 +405,4 @@ Lastly, checkpointing also provides fault-tolerance and error recovery: if one o
#### Pending writes
-Additionally, when a graph node fails mid-execution at a given superstep, LangGraph stores pending checkpoint writes from any other nodes that completed successfully at that superstep, so that whenever we resume graph execution from that superstep we don't re-run the successful nodes.
\ No newline at end of file
+Additionally, when a graph node fails mid-execution at a given superstep, LangGraph stores pending checkpoint writes from any other nodes that completed successfully at that superstep, so that whenever we resume graph execution from that superstep we don't re-run the successful nodes.
From 7a80d6cb87bd0c99f94f040d75faae35a11422ef Mon Sep 17 00:00:00 2001
From: Vadym Barda
Date: Tue, 3 Dec 2024 16:34:08 -0500
Subject: [PATCH 100/149] sdk-py: release 0.1.42 (#2612)
---
libs/sdk-py/pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml
index 3adaa531b..edf8a2510 100644
--- a/libs/sdk-py/pyproject.toml
+++ b/libs/sdk-py/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
-version = "0.1.41"
+version = "0.1.42"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"
From 5e3c32642406f7849a2aa91b4f9dd4b10909dda2 Mon Sep 17 00:00:00 2001
From: Vadym Barda
Date: Tue, 3 Dec 2024 16:38:59 -0500
Subject: [PATCH 101/149] langgraph: bump sdk, release 0.2.54 (#2613)
---
libs/langgraph/poetry.lock | 26 +++++++-------------------
libs/langgraph/pyproject.toml | 4 ++--
2 files changed, 9 insertions(+), 21 deletions(-)
diff --git a/libs/langgraph/poetry.lock b/libs/langgraph/poetry.lock
index 5005e8149..634b9d8db 100644
--- a/libs/langgraph/poetry.lock
+++ b/libs/langgraph/poetry.lock
@@ -791,17 +791,6 @@ cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
-[[package]]
-name = "httpx-sse"
-version = "0.4.0"
-description = "Consume Server-Sent Event (SSE) messages with HTTPX."
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"},
- {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"},
-]
-
[[package]]
name = "idna"
version = "3.10"
@@ -1359,7 +1348,7 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
-version = "2.0.4"
+version = "2.0.8"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1393,7 +1382,7 @@ url = "../checkpoint-duckdb"
[[package]]
name = "langgraph-checkpoint-postgres"
-version = "2.0.2"
+version = "2.0.7"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1401,10 +1390,10 @@ files = []
develop = true
[package.dependencies]
-langgraph-checkpoint = "^2.0.2"
+langgraph-checkpoint = "^2.0.7"
orjson = ">=3.10.1"
-psycopg = "^3.0.0"
-psycopg-pool = "^3.0.0"
+psycopg = "^3.2.0"
+psycopg-pool = "^3.2.0"
[package.source]
type = "directory"
@@ -1429,7 +1418,7 @@ url = "../checkpoint-sqlite"
[[package]]
name = "langgraph-sdk"
-version = "0.1.36"
+version = "0.1.42"
description = "SDK for interacting with LangGraph API"
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1438,7 +1427,6 @@ develop = true
[package.dependencies]
httpx = ">=0.25.2"
-httpx-sse = ">=0.4.0"
orjson = ">=3.10.1"
[package.source]
@@ -3425,4 +3413,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.9.0,<4.0"
-content-hash = "9bf5668d3f70f3b77457906732404a6401583a5966f70a72ef10a68f2a5b27ad"
+content-hash = "2df4d5d5e61917bdfff0ba430067a17662666eedee2858d841fa02e594cf69d0"
diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml
index 6db05f90d..e66600939 100644
--- a/libs/langgraph/pyproject.toml
+++ b/libs/langgraph/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
-version = "0.2.53"
+version = "0.2.54"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
@@ -11,7 +11,7 @@ repository = "https://www.github.com/langchain-ai/langgraph"
python = ">=3.9.0,<4.0"
langchain-core = ">=0.2.43,<0.4.0,!=0.3.0,!=0.3.1,!=0.3.2,!=0.3.3,!=0.3.4,!=0.3.5,!=0.3.6,!=0.3.7,!=0.3.8,!=0.3.9,!=0.3.10,!=0.3.11,!=0.3.12,!=0.3.13,!=0.3.14"
langgraph-checkpoint = "^2.0.4"
-langgraph-sdk = "^0.1.32"
+langgraph-sdk = "^0.1.42"
[tool.poetry.group.dev.dependencies]
pytest = "^8.3.2"
From a203ddecf73df6d43ac1a31482a6c90ce8e7e6ce Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Tue, 3 Dec 2024 15:48:59 -0800
Subject: [PATCH 102/149] Handle Command returned from node (in addition to
GraphCommand)
---
libs/langgraph/langgraph/graph/state.py | 20 +++++++++++---------
libs/langgraph/tests/test_pregel.py | 2 +-
libs/langgraph/tests/test_pregel_async.py | 4 ++--
3 files changed, 14 insertions(+), 12 deletions(-)
diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py
index 508f4e474..d1a503092 100644
--- a/libs/langgraph/langgraph/graph/state.py
+++ b/libs/langgraph/langgraph/graph/state.py
@@ -829,15 +829,16 @@ def _coerce_state(schema: Type[Any], input: dict[str, Any]) -> dict[str, Any]:
def _control_branch(value: Any) -> Sequence[Union[str, Send]]:
if isinstance(value, Send):
return [value]
- if not isinstance(value, GraphCommand):
+ if not isinstance(value, Command):
return EMPTY_SEQ
if value.graph == Command.PARENT:
raise ParentCommand(value)
rtn: list[Union[str, Send]] = []
- if isinstance(value.goto, str):
- rtn.append(value.goto)
- else:
- rtn.extend(value.goto)
+ if isinstance(value, GraphCommand):
+ if isinstance(value.goto, str):
+ rtn.append(value.goto)
+ else:
+ rtn.extend(value.goto)
if isinstance(value.send, Send):
rtn.append(value.send)
else:
@@ -853,10 +854,11 @@ async def _acontrol_branch(value: Any) -> Sequence[Union[str, Send]]:
if value.graph == Command.PARENT:
raise ParentCommand(value)
rtn: list[Union[str, Send]] = []
- if isinstance(value.goto, str):
- rtn.append(value.goto)
- else:
- rtn.extend(value.goto)
+ if isinstance(value, GraphCommand):
+ if isinstance(value.goto, str):
+ rtn.append(value.goto)
+ else:
+ rtn.extend(value.goto)
if isinstance(value.send, Send):
rtn.append(value.send)
else:
diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py
index 6d1caa342..4ff56ca3e 100644
--- a/libs/langgraph/tests/test_pregel.py
+++ b/libs/langgraph/tests/test_pregel.py
@@ -1925,7 +1925,7 @@ def test_send_sequences() -> None:
def send_for_fun(state):
return [
- Send("2", GraphCommand(send=Send("2", 3))),
+ Send("2", Command(send=Send("2", 3))),
Send("2", GraphCommand(send=Send("2", 4))),
"3.1",
]
diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py
index 378ca1ac1..cc244c363 100644
--- a/libs/langgraph/tests/test_pregel_async.py
+++ b/libs/langgraph/tests/test_pregel_async.py
@@ -2573,14 +2573,14 @@ async def test_send_sequences(checkpointer_name: str) -> None:
if isinstance(state, list) # or isinstance(state, Control)
else ["|".join((self.name, str(state)))]
)
- if isinstance(state, GraphCommand):
+ if isinstance(state, Command):
return replace(state, update=update)
else:
return update
async def send_for_fun(state):
return [
- Send("2", GraphCommand(send=Send("2", 3))),
+ Send("2", Command(send=Send("2", 3))),
Send("2", GraphCommand(send=Send("2", 4))),
"3.1",
]
From 1bee33db3af3c5937f6b3838197b7cf33668f38f Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Tue, 3 Dec 2024 15:52:41 -0800
Subject: [PATCH 103/149] Fix
---
libs/langgraph/langgraph/graph/state.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py
index d1a503092..1a7208a2a 100644
--- a/libs/langgraph/langgraph/graph/state.py
+++ b/libs/langgraph/langgraph/graph/state.py
@@ -849,7 +849,7 @@ def _control_branch(value: Any) -> Sequence[Union[str, Send]]:
async def _acontrol_branch(value: Any) -> Sequence[Union[str, Send]]:
if isinstance(value, Send):
return [value]
- if not isinstance(value, GraphCommand):
+ if not isinstance(value, Command):
return EMPTY_SEQ
if value.graph == Command.PARENT:
raise ParentCommand(value)
From 5fa196ab38d2a7530149ef4331d0f178c3d074cc Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Tue, 3 Dec 2024 19:51:25 -0800
Subject: [PATCH 104/149] Update docstrings for store classes (#2616)
---
...3-a466-46ad-aafe-2b870831057e.msgpack.zlib | 2 +-
...b-d730-48bd-9652-983812fd7811.msgpack.zlib | 2 +-
...0-1f8a-4057-81c4-b7bf073dc4c1.msgpack.zlib | 2 +-
docs/docs/cloud/deployment/semantic_search.md | 123 ++++++++++++
docs/docs/concepts/memory.md | 30 ++-
.../how-tos/cross-thread-persistence.ipynb | 15 +-
docs/docs/how-tos/index.md | 2 +
.../langgraph/store/postgres/aio.py | 62 ++++++
.../langgraph/store/postgres/base.py | 46 +++++
.../langgraph/store/base/__init__.py | 184 +++++++++++++++---
.../langgraph/store/memory/__init__.py | 118 ++++++++---
libs/sdk-js/src/client.ts | 3 +
libs/sdk-js/src/schema.ts | 12 +-
13 files changed, 533 insertions(+), 68 deletions(-)
create mode 100644 docs/docs/cloud/deployment/semantic_search.md
diff --git a/docs/cassettes/cross-thread-persistence_c871a073-a466-46ad-aafe-2b870831057e.msgpack.zlib b/docs/cassettes/cross-thread-persistence_c871a073-a466-46ad-aafe-2b870831057e.msgpack.zlib
index 923a74528..1253235cc 100644
--- a/docs/cassettes/cross-thread-persistence_c871a073-a466-46ad-aafe-2b870831057e.msgpack.zlib
+++ b/docs/cassettes/cross-thread-persistence_c871a073-a466-46ad-aafe-2b870831057e.msgpack.zlib
@@ -1 +1 @@
-eNqFVG1sU1UYLkIIGkA0GkkY7Fid8Qenve1tR9uQYO0Ax6gbW0GKEXJ27rnttfeLe07HPiTKwKAjkVxJGCrG6LqWlA26AGrAKYmQGPWHhhgsBELUGP/4C6KJBua5XcsgW2Z/NPee9+t5n+c5t7/QRSyqGPqcEUVnxEKY8Rdq9xcssjNLKNuX1whLG1KurbUjMZS1lHJDmjGTRrxeZCoepLO0ZZgK9mBD83b5vBqhFKUIzXUaUk/5hz63hrp3MCNDdOqOAJ/gD6wE7loSP3m5z20ZKuFP7iwllptHscGR6Mw5ekF5ErQTjWidxIoArQfoSCNAoeB5o9O9+xWnlSER1UnFKspKBIowCKmh64RBPx8mNPoFpyftoYxoTl7SyAJkEYBAmqimnFUBolShjG8CGFIzip4CzAAsTYADyAM283+g6LLBi3cX0gRJnLGDubRBmT02jYOTCGNiMkh0bEi8lz2a6lXMlUAisooYKWIHW4Vku5ghxIRIVbpIfrLKLiHTVBWMnLj3Vb7HSJUMyHpMMj1cdCBCTqXO7M+iNRzeth4umQ4Ejxjw+EvdkG+n6ConHaqIQ8qblfi5ewMmwhneB1btYOcni0/cm2NQeziOcGvHfS2RhdP2MLK0xsCpe8+trM4UjdiFWNv0cdXg1DjR4/N5wmP3NaY9OraHZaRSMnaX5LslRa6vCIVGKPhO1FhSiZ5iaXvIL4SOWYSa3Mpkb563ZFnan+OKkO+/KVTd90lrS03N665Hc01cHXt8naWsBEIAtGIGHP8AXyjiD0cEP1gfT4zEqmMSM4oxlrCQTmUuyNqa+AWczuoZIhVjM8pedk+tZfH5qqIpDFZvHhfLebVzAUEQys/MmmkRjbPmTMyJ4XD4f/pyZgizTzv7QZ8AhUBicstgYFsZzFQ5eX+rePIOHo7o6Vkyp/DUssGs2TPjEfzbilXQUJHsL/jzDsHXviqeifokRuM0bMbFYCiTNMPBM90Qq0ZWgox/wwisGKKb2WXQKRA50BiSRTEoiqIkBcOyLAeCZJUoyVK4kQx1Kcgu+jw+kDKMlEpOxtbBGMJpAjsqtrELTckXo/Hm2MhW2G50Gpy/BOI864ZO8h3E4na0i5XR/IJbJM/L26NJ+3QIS6tE7MeBkBwMiGEM177UXqoZ6K5Bcs7XofKt3MNtavGji3OW1B9Y4Kr85iYORlu+fm7JmxPfhga3N767Dp59IvrYlQMLBgbc6NDCIBM7dsmjw7e+aimuWL3otZvXfx1f/tfy3jqWHLx0NXmz+9LqNQmvcqb52q2bvXSC7V+snctvWltEzX8IzXXRtj9/emBZpC439NaKq0ut9Rebd245Jhx6fHPyw8Onik37R0uPLPvn8rUdx368nF5Td5AObLqx4f2ntv7ctOffw+fnzXfvHf0Nb93Yua9p7qJFDaW/zRsPhhbOG3/nOj7y3fwr50tvfDQwNG/j2dL4hdsfxILJy2/3PzS0/pcTnx5d+nr9hvr3Pm+4dRSzIw8fv724b3vqeHzLhYEvI7ueLd/5+MDtO8tcromJua7fPfHBjjku1385MZ/f
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/docs/cassettes/cross-thread-persistence_d362350b-d730-48bd-9652-983812fd7811.msgpack.zlib b/docs/cassettes/cross-thread-persistence_d362350b-d730-48bd-9652-983812fd7811.msgpack.zlib
index 795ff5279..3675bd448 100644
--- a/docs/cassettes/cross-thread-persistence_d362350b-d730-48bd-9652-983812fd7811.msgpack.zlib
+++ b/docs/cassettes/cross-thread-persistence_d362350b-d730-48bd-9652-983812fd7811.msgpack.zlib
@@ -1 +1 @@
-eNqFVHlsFFUcXiyIgTYg3gFkWO7j7c7sbrtsRcyy3KVsabdCMUJe37zdme7szHTe2x5UgpRLhEgmHiTGEAPbXdm00NoiIaAGFAE1QkgIbEgANQT/kIQQDFY5fLPd5QgNzh+Tmd/5/b7f915rqgEbRNbUAe2ySrEBEWU/xGxNGbg+jgndkIxhKmlioiJYFdodN+TMBIlSnZQ6nVCXHVClkqHpMnIgLeZsEJwxTAiMYJKo1cTmzPct9hhsWkW1KFaJvZQTeJdnOmfPBzHLWy12Q1Mw+7LHCTbszIs0hkSllqlRgpSTCRdr5lQYw2/Y17xtpWsiViw3UmBcxMANigHRVBVT4GIN+BIXb9UhzYTimBVXo8U5aGAOchJW9HBc4SAhMqEMPUehEpXVCEc1jkqYs0A4uGr25mQ1rLHkNSkJQ5GxtD0haYSaXY/NvQ8ihHUKsIo0kdUyOyKrZX06J+KwAilOIwtbllgzHcVYB1CRG3CyL8vshLquyAhafmcdm6M9RwCgzTp+3J22IAJGn0rNA/48DmdFM1uTyvEOt8fh6mwCbDpZVRjRQIEMUlLP+g897NAhirI6ICcBM9mXvPfhGI2YbeUQBaseKQkNJJlt0IiVeLofthtxlcoxbKYCFY+3yzkftHM7BMHh63qkMGlWkdkWhgrBXfdJvp+SZvt1A74E8MLePEsKViNUMncLvpIvDEx0Jl+8PslK0jhpTbCN4J9PpHKK2xUsy2/zom1EYg7bjvn1PEOezvEeLogoZ+mHE2aUunylzDK/PNQeyLUJ9buMrpABVRJmC5mbX34KSXE1isV0oN+1Z+wPxjJYf0WOyRTkThtblvVrJjw8z2cmPjHSwDHGmtUx4fb5fP9TlzGDqdljzQcEHvCeUN+UxZ4VGa6/zL4zm8OTtPAwROOfEPkATz6ae2J0/3h4z4p0DjSQRfMw+17FCxXRCEKLPdF6b039kmhZXXFj06Jo/f4mgBQtLgLK7i0MsoJoomaG87p8XkEQisPe8AyvKIphEYsC9kIXqnXBWgR3N8jQTAsOgYtoWkTB+wLzQAAiCYOqrGzM1JyaJf7yhYH25aBSq9UYfyHIeFY1FSersMHkaKazrdkBN3CSpVf6a8yeGUj0upHb40VY9Lh9CMxdVtmZF9B9gSSs2yF7P65jMjWY6diAyJitz9iyT4Fi+qMj/YUb771/+tiOev2r+OQWtw8c3DBqSGxKeOa4C4Fw28kFvzeg25d++jQJJkzakkoFh9Efnu86vnTYhbPXrg5effuj386du44O9M6+9c1rsz4uKu3Zc/pPe/Vaji6fcKp1WXndzqLRH4yYgraP/fdFuRceXJnmPwSHqm/Vx891nIbDS2q/u3uht6juJln57SevDr4+98tRZ6b5zhevH7Dr+tHLfwQH7py04PCO0tVlRy83nimcKL9wfjstGDnoVOcrl4OuW2PGDqwc9NzkUPX5q43vlu+Z6vmluzgz2z90Y0/wyNWZ/j3GItM+flMvHT5r6dlpvz696c4o6dmxqYhBhh1xv3ylcNydoqE7xxVu/QzO12+2LSbp5MCTF08IspR2v+S8NDq9MPPmjanbXh9xvGP/j3+3fH7zzHsF8BSIbL72T/vdbVvKujtOurtqL3WMXtS9tu3QX1duDLXZ7t0rsCWnrXtnyFM223+sv8iH
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/docs/cassettes/cross-thread-persistence_d862be40-1f8a-4057-81c4-b7bf073dc4c1.msgpack.zlib b/docs/cassettes/cross-thread-persistence_d862be40-1f8a-4057-81c4-b7bf073dc4c1.msgpack.zlib
index 7a4f441ea..290b07eca 100644
--- a/docs/cassettes/cross-thread-persistence_d862be40-1f8a-4057-81c4-b7bf073dc4c1.msgpack.zlib
+++ b/docs/cassettes/cross-thread-persistence_d862be40-1f8a-4057-81c4-b7bf073dc4c1.msgpack.zlib
@@ -1 +1 @@
-eNqFVG1oHEUYvlDQ+sMPqggGwfFoU5HM3e7d5i4XBY2XxLQ1HyZX8iGlndudvd3e7uy6O5fetUZqEm1sSdulCIKlBXO5K9eYJm3qB7Ui2B+KFhTqj6MqVBAs8YcW/KxtnL3cJSkJcVmW2Xm/nvd5n5mh/AC2bNUgVZMqodhCImU/tjOUt/DLKWzTkZyOqWJI2c6O7th4ylKLmxRKTbvB70em6kOEKpZhqqJPNHT/AO/XsW2jBLazcUPKFK/s8+oovZMaSUxsbwPguYBQC7wVJ7bz0j6vZWiYrbwpG1teZhUNhoRQd2uPgihQbaBnAEE6fsY7uMMNNySsuWZRQykJwyCsg7ZBCKYwwApwoQDn5rEzNsW669dnpACyMEBAwZoppzSAbFu1KUMPKNKSKkkAagCqYOCC8IHt7AtUIhsNC0u3uIvjOSPuHcwrGEmMtSNZxbCpM7OChzNIFLFJISaiIbHcznuJvapZCyQsa4jiguhiLRHtFJIYmxBp6gDOLUQ508g0NVVErt2/m/U1WSYE0oyJV5oLLmTI6CTU+aCxgsPfmWFjI4DzBQVfYDoNWbcq0RjxUEMMUs4s2S8sN5hITLI8sCwJJ7cQPLXcx7CdiTYkdnTfkRJZouJMIEsPCeeW71spQlUdO/lo58pyZeNSuaCP532RmTsS2xkiOhMy0mw8s0jyYkiBzTsIuRDk+KkKSxomCao44wE+cMrCtsnkjIdzLCVN2UNZNhH81ef5sgLf7dhWmeYPng3ZJjYd52KLpdYCTgAdIgWungBf3xCINHAB8HxbbDJaLhNbdRgzMQsRW2YDaa4MPy8qKZLEUiG66tiL3qW2LFZfU3WVwvLpY8Nyf52swHFcsWZNTwvrjDW3YjYYiUT+Jy9jBlNn1u0P8hzkhNhCl3VCfxGsFrlwhst4ci4ehmjjGp5LeCreYE3v1fFwgf5CGTRUJedjtt7J8W1Iifa0b+sVd0vp7e2tAdqeDCvy+TQUNSMlQcruMQxLgkhTpwjiclyuY28wGBLCKBQOcjyW5XBIkgUUiGBhfEBFToH38SBhGAkNn4m2wCgSFQy7S7Jx8k197Y1tW6KTvbDLiBuMvxhiPBOD4Fw3tpgcnUKpNDvgFs6x8K7GPme2XpTCQTHIx+vrOSEYEWFzT9d0RUCLAsm6t0PpvnyNydRiW5euPnZovaf0rHvh8FPJz559YGT+4Nv/PCQVM39t7al5/7TAPz56iOgjEzuaLyQ2jNzed3QL+PbT8P1y04PXvrn+0ZMPX50jF38+4c/fuvGr9f0vP8798Vt6//Dgn0K+te74PfcNzwbXF8b6dw1HNzv96St6q3MDv3ryNHfsJ/zi1MEDZ/dHoXbprvnQ7bHqwcu/v5N+dNO9jxw4fLbl3Ngr36G9ezqPfVL9xCnu6VERq7v+rd568s0Pa9Lt+txRsvnW6Bs3pbe+eL332pfnhzfePHIid/1uj2d+fp3n76+rL1dVeTz/AbjckGA=
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/docs/docs/cloud/deployment/semantic_search.md b/docs/docs/cloud/deployment/semantic_search.md
new file mode 100644
index 000000000..918d0e3df
--- /dev/null
+++ b/docs/docs/cloud/deployment/semantic_search.md
@@ -0,0 +1,123 @@
+# How to add semantic search to your LangGraph deployment
+
+This guide explains how to add semantic search to your LangGraph deployment's cross-thread [store](../../concepts/persistence.md#memory-store), so that your agent can search for memories and other documents by semantic similarity.
+
+## Prerequisites
+
+- A LangGraph deployment (see [how to deploy](setup_pyproject.md))
+- API keys for your embedding provider (in this case, OpenAI)
+- `langchain >= 0.3.8` (if you specify using the string format below)
+
+## Steps
+
+1. Update your `langgraph.json` configuration file to include the store configuration:
+
+```json
+{
+ ...
+ "store": {
+ "index": {
+ "embed": "openai:text-embeddings-3-small",
+ "dims": 1536,
+ "fields": ["$"]
+ }
+ }
+}
+```
+
+This configuration:
+
+- Uses OpenAI's text-embeddings-3-small model for generating embeddings
+- Sets the embedding dimension to 1536 (matching the model's output)
+- Indexes all fields in your stored data (`["$"]` means index everything, or specify specific fields like `["text", "metadata.title"]`)
+
+2. To use the string embedding format above, make sure your dependencies include `langchain >= 0.3.8`:
+
+```toml
+# In pyproject.toml
+[project]
+dependencies = [
+ "langchain>=0.3.8"
+]
+```
+
+Or if using requirements.txt:
+
+```
+langchain>=0.3.8
+```
+
+## Usage
+
+Once configured, you can use semantic search in your LangGraph nodes. The store requires a namespace tuple to organize memories:
+
+```python
+def search_memory(state: State, *, store: BaseStore):
+ # Search the store using semantic similarity
+ # The namespace tuple helps organize different types of memories
+ # e.g., ("user_facts", "preferences") or ("conversation", "summaries")
+ results = store.search(
+ namespace=("memory", "facts"), # Organize memories by type
+ query="your search query",
+ k=3 # number of results to return
+ )
+ return results
+```
+
+## Custom Embeddings
+
+If you want to use custom embeddings, you can pass a path to a custom embedding function:
+
+```json
+{
+ ...
+ "store": {
+ "index": {
+ "embed": "path/to/embedding_function.py:embed",
+ "dims": 1536,
+ "fields": ["$"]
+ }
+ }
+}
+```
+
+The deployment will look for the function in the specified path. The function must be async and accept a list of strings:
+
+```python
+# path/to/embedding_function.py
+from openai import AsyncOpenAI
+
+client = AsyncOpenAI()
+
+async def aembed_texts(texts: list[str]) -> list[list[float]]:
+ """Custom embedding function that must:
+ 1. Be async
+ 2. Accept a list of strings
+ 3. Return a list of float arrays (embeddings)
+ """
+ response = await client.embeddings.create(
+ model="text-embedding-3-small",
+ input=texts
+ )
+ return [e.embedding for e in response.data]
+```
+
+## Querying via the API
+
+You can also query the store using the LangGraph SDK. Since the SDK uses async operations:
+
+```python
+from langgraph_sdk import get_client
+
+async def search_store():
+ client = get_client()
+ results = await client.store.search(
+ namespace=("memory", "facts"),
+ query="your search query",
+ limit=3 # number of results to return
+ )
+ return results
+
+# Use in an async context
+results = await search_store()
+```
diff --git a/docs/docs/concepts/memory.md b/docs/docs/concepts/memory.md
index 49eb8e118..126b35993 100644
--- a/docs/docs/concepts/memory.md
+++ b/docs/docs/concepts/memory.md
@@ -171,7 +171,7 @@ trim_messages(
## Long-term memory
-Long-term memory in LangGraph allows systems to retain information across different conversations or sessions. Unlike short-term memory, which is thread-scoped, long-term memory is saved within custom "namespaces."
+Long-term memory in LangGraph allows systems to retain information across different conversations or sessions. Unlike short-term memory, which is **thread-scoped**, long-term memory is saved within custom "namespaces."
### Storing memories
@@ -180,16 +180,34 @@ LangGraph stores long-term memories as JSON documents in a [store](persistence.m
```python
from langgraph.store.memory import InMemoryStore
+
+def embed(texts: list[str]) -> list[list[float]]:
+ # Replace with an actual embedding function or LangChain embeddings object
+ return [[1.0, 2.0] * len(texts)]
+
+
# InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use.
-store = InMemoryStore()
+store = InMemoryStore(index={"embed": embed, "dims": 2})
user_id = "my-user"
application_context = "chitchat"
namespace = (user_id, application_context)
-store.put(namespace, "a-memory", {"rules": ["User likes short, direct language", "User only speaks English & python"], "my-key": "my-value"})
+store.put(
+ namespace,
+ "a-memory",
+ {
+ "rules": [
+ "User likes short, direct language",
+ "User only speaks English & python",
+ ],
+ "my-key": "my-value",
+ },
+)
# get the "memory" by ID
item = store.get(namespace, "a-memory")
-# list "memories" within this namespace, filtering on content equivalence
-items = store.search(namespace, filter={"my-key": "my-value"})
+# search for "memories" within this namespace, filtering on content equivalence, sorted by vector similarity
+items = store.search(
+ namespace, filter={"my-key": "my-value"}, query="language preferences"
+)
```
### Framework for thinking about long-term memory
@@ -232,7 +250,7 @@ Alternatively, memories can be a collection of documents that are continuously u
However, this shifts some complexity memory updating. The model must now _delete_ or _update_ existing items in the list, which can be tricky. In addition, some models may default to over-inserting and others may default to over-updating. See the [Trustcall](https://github.com/hinthornw/trustcall) package for one way to manage this and consider evaluation (e.g., with a tool like [LangSmith](https://docs.smith.langchain.com/tutorials/Developers/evaluation)) to help you tune the behavior.
-Working with document collections also shifts complexity to memory **search** over the list. The `Store` currently supports [filtering by metadata](https://langchain-ai.github.io/langgraph/reference/store/#storage) and will soon add [semantic search shortly](https://python.langchain.com/docs/concepts/vectorstores/), but selecting the most relevant documents can be tricky as the list grows.
+Working with document collections also shifts complexity to memory **search** over the list. The `Store` currently supports both [semantic search](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.SearchOp.query) and [filtering by content](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.SearchOp.filter).
Finally, using a collection of memories can make it challenging to provide comprehensive context to the model. While individual memories may follow a specific schema, this structure might not capture the full context or relationships between memories. As a result, when using these memories to generate responses, the model may lack important contextual information that would be more readily available in a unified profile approach.
diff --git a/docs/docs/how-tos/cross-thread-persistence.ipynb b/docs/docs/how-tos/cross-thread-persistence.ipynb
index da728b24e..ac6c657f2 100644
--- a/docs/docs/how-tos/cross-thread-persistence.ipynb
+++ b/docs/docs/how-tos/cross-thread-persistence.ipynb
@@ -41,6 +41,9 @@
" \n",
" Support for the Store API that is used in this guide was added in LangGraph v0.2.32.\n",
"
\n",
+ " \n",
+ " Support for index and query arguments of the Store API that is used in this guide was added in LangGraph v0.2.54.\n",
+ "
\n",
"\n",
"\n",
"## Setup\n",
@@ -114,7 +117,7 @@
"\n",
"Importantly, to determine the user, we will be passing `user_id` via the config keyword argument of the node function.\n",
"\n",
- "Let's first define an `InMemoryStore` which is already populated with some memories about the users."
+ "Let's first define an `InMemoryStore` already populated with some memories about the users."
]
},
{
@@ -125,8 +128,14 @@
"outputs": [],
"source": [
"from langgraph.store.memory import InMemoryStore\n",
+ "from langchain_openai import OpenAIEmbeddings\n",
"\n",
- "in_memory_store = InMemoryStore()"
+ "in_memory_store = InMemoryStore(\n",
+ " index={\n",
+ " \"embed\": OpenAIEmbeddings(model=\"text-embedding-3-small\"),\n",
+ " \"dims\": 1536,\n",
+ " }\n",
+ ")"
]
},
{
@@ -163,7 +172,7 @@
"def call_model(state: MessagesState, config: RunnableConfig, *, store: BaseStore):\n",
" user_id = config[\"configurable\"][\"user_id\"]\n",
" namespace = (\"memories\", user_id)\n",
- " memories = store.search(namespace)\n",
+ " memories = store.search(namespace, query=str(state[\"messages\"][-1].content))\n",
" info = \"\\n\".join([d.value[\"data\"] for d in memories])\n",
" system_msg = f\"You are a helpful assistant talking to the user. User info: {info}\"\n",
"\n",
diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md
index 1419fc1f2..f1c0f1a51 100644
--- a/docs/docs/how-tos/index.md
+++ b/docs/docs/how-tos/index.md
@@ -39,6 +39,7 @@ LangGraph makes it easy to manage conversation [memory](../concepts/memory.md) i
- [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)
+- [Add long-term memory (cross-thread)](cross-thread-persistence.ipynb)
### Human-in-the-loop
@@ -139,6 +140,7 @@ Learn how to set up your app for deployment to LangGraph Platform:
- [How to set up app for deployment (requirements.txt)](../cloud/deployment/setup.md)
- [How to set up app for deployment (pyproject.toml)](../cloud/deployment/setup_pyproject.md)
- [How to set up app for deployment (JavaScript)](../cloud/deployment/setup_javascript.md)
+- [How to add semantic search](../cloud/deployment/semantic_search.md)
- [How to customize Dockerfile](../cloud/deployment/custom_docker.md)
- [How to test locally](../cloud/deployment/test_locally.md)
- [How to rebuild graph at runtime](../cloud/deployment/graph_rebuild.md)
diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/aio.py b/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
index 282f08186..9be44ded6 100644
--- a/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
+++ b/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
@@ -37,6 +37,68 @@ logger = logging.getLogger(__name__)
class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Conn]):
+ """Asynchronous Postgres-backed store with optional vector search using pgvector.
+
+ !!! example "Examples"
+ Basic setup and key-value storage:
+ ```python
+ from langgraph.store.postgres import AsyncPostgresStore
+
+ async with AsyncPostgresStore.from_conn_string(
+ "postgresql://user:pass@localhost:5432/dbname"
+ ) as store:
+ await store.setup()
+
+ # Store and retrieve data
+ await store.aput(("users", "123"), "prefs", {"theme": "dark"})
+ item = await store.aget(("users", "123"), "prefs")
+ ```
+
+ Vector search using LangChain embeddings:
+ ```python
+ from langchain.embeddings import init_embeddings
+ from langgraph.store.postgres import AsyncPostgresStore
+
+ async with AsyncPostgresStore.from_conn_string(
+ "postgresql://user:pass@localhost:5432/dbname",
+ index={
+ "dims": 1536,
+ "embed": init_embeddings("openai:text-embedding-3-small"),
+ "fields": ["text"] # specify which fields to embed. Default is the whole serialized value
+ }
+ ) as store:
+ await store.setup() # Do this once to run migrations
+
+ # Store documents
+ await store.aput(("docs",), "doc1", {"text": "Python tutorial"})
+ await store.aput(("docs",), "doc2", {"text": "TypeScript guide"})
+
+ # Search by similarity
+ results = await store.asearch(("docs",), query="python programming")
+ ```
+
+ Using connection pooling for better performance:
+ ```python
+ from langgraph.store.postgres import AsyncPostgresStore, PoolConfig
+
+ async with AsyncPostgresStore.from_conn_string(
+ "postgresql://user:pass@localhost:5432/dbname",
+ pool_config=PoolConfig(
+ min_size=5,
+ max_size=20
+ )
+ ) as store:
+ await store.setup()
+ # Use store with connection pooling...
+ ```
+
+ Warning:
+ Make sure to:
+ 1. Call `setup()` before first use to create necessary tables and indexes
+ 2. Have the pgvector extension available to use vector search
+ 3. Use Python 3.10+ for async functionality
+ """
+
__slots__ = (
"_deserializer",
"pipe",
diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/base.py b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
index 28edd8998..e12a59667 100644
--- a/libs/checkpoint-postgres/langgraph/store/postgres/base.py
+++ b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
@@ -534,6 +534,52 @@ class BasePostgresStore(Generic[C]):
class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
+ """Postgres-backed store with optional vector search using pgvector.
+
+ !!! example "Examples"
+ Basic setup and key-value storage:
+ ```python
+ from langgraph.store.postgres import PostgresStore
+
+ store = PostgresStore(
+ connection_string="postgresql://user:pass@localhost:5432/dbname"
+ )
+ store.setup()
+
+ # Store and retrieve data
+ store.put(("users", "123"), "prefs", {"theme": "dark"})
+ item = store.get(("users", "123"), "prefs")
+ ```
+
+ Vector search using LangChain embeddings:
+ ```python
+ from langchain.embeddings import init_embeddings
+ from langgraph.store.postgres import PostgresStore
+
+ store = PostgresStore(
+ connection_string="postgresql://user:pass@localhost:5432/dbname",
+ index={
+ "dims": 1536,
+ "embed": init_embeddings("openai:text-embedding-3-small"),
+ "fields": ["text"] # specify which fields to embed. Default is the whole serialized value
+ }
+ )
+ store.setup() # Do this once to run migrations
+
+ # Store documents
+ store.put(("docs",), "doc1", {"text": "Python tutorial"})
+ store.put(("docs",), "doc2", {"text": "TypeScript guide"})
+
+ # Search by similarity
+ results = store.search(("docs",), query="python programming")
+ ```
+
+ Warning:
+ Make sure to call `setup()` before first use to create necessary tables and indexes.
+ The pgvector extension must be available to use vector search.
+
+ """
+
__slots__ = (
"_deserializer",
"pipe",
diff --git a/libs/checkpoint/langgraph/store/base/__init__.py b/libs/checkpoint/langgraph/store/base/__init__.py
index 033cfba12..9673fc6cb 100644
--- a/libs/checkpoint/langgraph/store/base/__init__.py
+++ b/libs/checkpoint/langgraph/store/base/__init__.py
@@ -4,9 +4,9 @@ Stores provide long-term memory that persists across threads and conversations.
Supports hierarchical namespaces, key-value storage, and optional vector search.
Core types:
-- BaseStore: Store interface with sync/async operations
-- Item: Stored key-value pairs with metadata
-- Op: Get/Put/Search/List operations
+ - BaseStore: Store interface with sync/async operations
+ - Item: Stored key-value pairs with metadata
+ - Op: Get/Put/Search/List operations
"""
from abc import ABC, abstractmethod
@@ -89,7 +89,7 @@ class Item:
class SearchItem(Item):
- """Represents a result item with additional response metadata."""
+ """Represents an item returned from a search operation with additional metadata."""
__slots__ = ("score",)
@@ -133,7 +133,7 @@ class GetOp(NamedTuple):
This operation allows precise retrieval of stored items using their full path
(namespace) and unique identifier (key) combination.
- ??? example "Examples"
+ ???+example "Examples"
Basic item retrieval:
```python
@@ -145,7 +145,7 @@ class GetOp(NamedTuple):
namespace: tuple[str, ...]
"""Hierarchical path that uniquely identifies the item's location.
- ??? example "Examples"
+ ???+example "Examples"
```python
("users",) # Root level users namespace
@@ -156,7 +156,7 @@ class GetOp(NamedTuple):
key: str
"""Unique identifier for the item within its specific namespace.
- ??? example "Examples"
+ ???+example "Examples"
```python
"user123" # For a user profile
@@ -175,7 +175,7 @@ class SearchOp(NamedTuple):
Note:
Natural language search support depends on your store implementation.
- ??? example "Examples"
+ ???+example "Examples"
Search with filters and pagination:
```python
SearchOp(
@@ -199,7 +199,7 @@ class SearchOp(NamedTuple):
namespace_prefix: tuple[str, ...]
"""Hierarchical path prefix defining the search scope.
- ??? example "Examples"
+ ???+example "Examples"
```python
() # Search entire store
@@ -221,8 +221,7 @@ class SearchOp(NamedTuple):
- $lt: Less than
- $lte: Less than or equal to
- ??? example "Examples"
-
+ ???+example "Examples"
Simple exact match:
```python
@@ -243,9 +242,6 @@ class SearchOp(NamedTuple):
"color": "red"
}
```
-
- Note:
- Comparison operator support depends on your store implementation.
"""
limit: int = 10
@@ -257,7 +253,7 @@ class SearchOp(NamedTuple):
query: Optional[str] = None
"""Natural language search query for semantic search capabilities.
- ??? example "Examples"
+ ???+example "Examples"
- "technical documentation about REST APIs"
- "machine learning papers from 2023"
"""
@@ -267,10 +263,12 @@ class SearchOp(NamedTuple):
NamespacePath = tuple[Union[str, Literal["*"]], ...]
"""A tuple representing a namespace path that can include wildcards.
-Examples:
+???+example "Examples"
+ ```python
("users",) # Exact users namespace
("documents", "*") # Any sub-namespace under documents
("cache", "*", "v1") # Any cache category with v1 version
+ ```
"""
# Type for specifying how to match namespaces
@@ -290,7 +288,7 @@ class MatchCondition(NamedTuple):
pattern that can include wildcards to flexibly match different namespace
hierarchies.
- ??? example "Examples"
+ ???+example "Examples"
Prefix matching:
```python
MatchCondition(match_type="prefix", path=("users", "profiles"))
@@ -320,7 +318,7 @@ class ListNamespacesOp(NamedTuple):
This operation allows exploring the organization of data, finding specific
collections, and navigating the namespace hierarchy.
- ??? example "Examples"
+ ???+example "Examples"
List all namespaces under the "documents" path:
```python
@@ -343,7 +341,7 @@ class ListNamespacesOp(NamedTuple):
match_conditions: Optional[tuple[MatchCondition, ...]] = None
"""Optional conditions for filtering namespaces.
- ??? example "Examples"
+ ???+example "Examples"
All user namespaces:
```python
(MatchCondition(match_type="prefix", path=("users",)),)
@@ -385,7 +383,7 @@ class PutOp(NamedTuple):
The namespace acts as a folder-like structure to organize items.
Each element in the tuple represents one level in the hierarchy.
- ??? example "Examples"
+ ???+example "Examples"
Root level documents
```python
("documents",)
@@ -447,7 +445,7 @@ class PutOp(NamedTuple):
- Last element: "array[-1]"
- All elements (each individually): "array[*]"
- ??? example "Examples"
+ ???+example "Examples"
- None - Use store defaults
- False - Don't index this item
- list[str] - List of fields to index
@@ -490,7 +488,71 @@ class IndexConfig(TypedDict, total=False):
"""
embed: Union[Embeddings, EmbeddingsFunc, AEmbeddingsFunc]
- """Optional function to generate embeddings from text."""
+ """Optional function to generate embeddings from text.
+
+ Can be specified in three ways:
+ 1. A LangChain Embeddings instance
+ 2. A synchronous embedding function (EmbeddingsFunc)
+ 3. An asynchronous embedding function (AEmbeddingsFunc)
+
+ ???+example "Examples"
+ Using LangChain's initialization with InMemoryStore:
+ ```python
+ from langchain.embeddings import init_embeddings
+ from langgraph.store.memory import InMemoryStore
+
+ store = InMemoryStore(
+ index={
+ "dims": 1536,
+ "embed": init_embeddings("openai:text-embedding-3-small")
+ }
+ )
+ ```
+
+ Using a custom embedding function with InMemoryStore:
+ ```python
+ from openai import OpenAI
+ from langgraph.store.memory import InMemoryStore
+
+ client = OpenAI()
+
+ def embed_texts(texts: list[str]) -> list[list[float]]:
+ response = client.embeddings.create(
+ model="text-embedding-3-small",
+ input=texts
+ )
+ return [e.embedding for e in response.data]
+
+ store = InMemoryStore(
+ index={
+ "dims": 1536,
+ "embed": embed_texts
+ }
+ )
+ ```
+
+ Using an asynchronous embedding function with InMemoryStore:
+ ```python
+ from openai import AsyncOpenAI
+ from langgraph.store.memory import InMemoryStore
+
+ client = AsyncOpenAI()
+
+ async def aembed_texts(texts: list[str]) -> list[list[float]]:
+ response = await client.embeddings.create(
+ model="text-embedding-3-small",
+ input=texts
+ )
+ return [e.embedding for e in response.data]
+
+ store = InMemoryStore(
+ index={
+ "dims": 1536,
+ "embed": aembed_texts
+ }
+ )
+ ```
+ """
fields: Optional[list[str]]
"""Fields to extract text from for embedding generation.
@@ -565,6 +627,39 @@ class BaseStore(ABC):
Returns:
List of items matching the search criteria.
+
+ ???+ example "Examples"
+ Basic filtering:
+ ```python
+ # Search for documents with specific metadata
+ results = store.search(
+ ("docs",),
+ filter={"type": "article", "status": "published"}
+ )
+ ```
+
+ Natural language search (requires vector store implementation):
+ ```python
+ # Initialize store with embedding configuration
+ store = YourStore( # e.g., InMemoryStore, AsyncPostgresStore
+ index={
+ "dims": 1536, # embedding dimensions
+ "embed": your_embedding_function, # function to create embeddings
+ "fields": ["text"] # fields to embed
+ }
+ )
+
+ # Search for semantically similar documents
+ results = store.search(
+ ("docs",),
+ query="machine learning applications in healthcare",
+ filter={"type": "research_paper"},
+ limit=5
+ )
+ ```
+
+ Note: Natural language search support depends on your store implementation
+ and requires proper embedding configuration.
"""
return self.batch([SearchOp(namespace_prefix, filter, limit, offset, query)])[0]
@@ -596,13 +691,13 @@ class BaseStore(ABC):
Indexing capabilities depend on your store implementation.
Some implementations may support only a subset of indexing features.
- ??? example "Examples"
+ ???+example "Examples"
Simple storage without special indexing (respects store defaults)
```python
store.put(("docs",), "report", {"title": "Annual Report"})
```
- Index specific fields for search
+ Index specific fields for search (if store configured to index items)
```python
store.put(("docs",), "report", {"title": "Annual Report"}, index=["title"])
```
@@ -650,7 +745,7 @@ class BaseStore(ABC):
List[Tuple[str, ...]]: A list of namespace tuples that match the criteria.
Each tuple represents a full namespace path up to `max_depth`.
- ??? example "Examples":
+ ???+example "Examples":
Setting max_depth=3. Given the namespaces:
```python
# Example if you have the following namespaces:
@@ -710,6 +805,39 @@ class BaseStore(ABC):
Returns:
List of items matching the search criteria.
+
+ ???+ example "Examples"
+ Basic filtering:
+ ```python
+ # Search for documents with specific metadata
+ results = await store.asearch(
+ ("docs",),
+ filter={"type": "article", "status": "published"}
+ )
+ ```
+
+ Natural language search (requires vector store implementation):
+ ```python
+ # Initialize store with embedding configuration
+ store = YourStore( # e.g., InMemoryStore, AsyncPostgresStore
+ index={
+ "dims": 1536, # embedding dimensions
+ "embed": your_embedding_function, # function to create embeddings
+ "fields": ["text"] # fields to embed
+ }
+ )
+
+ # Search for semantically similar documents
+ results = await store.asearch(
+ ("docs",),
+ query="machine learning applications in healthcare",
+ filter={"type": "research_paper"},
+ limit=5
+ )
+ ```
+
+ Note: Natural language search support depends on your store implementation
+ and requires proper embedding configuration.
"""
return (
await self.abatch(
@@ -745,13 +873,13 @@ class BaseStore(ABC):
Indexing capabilities depend on your store implementation.
Some implementations may support only a subset of indexing features.
- ??? example "Examples"
+ ???+example "Examples"
Simple storage without special indexing:
```python
await store.aput(("docs",), "report", {"title": "Annual Report"})
```
- Index specific fields for search:
+ Index specific fields for search (if store configured to index items):
```python
await store.aput(
("docs",),
@@ -802,7 +930,7 @@ class BaseStore(ABC):
List[Tuple[str, ...]]: A list of namespace tuples that match the criteria.
Each tuple represents a full namespace path up to `max_depth`.
- ??? example "Examples"
+ ???+example "Examples"
Setting max_depth=3 with existing namespaces:
```python
# Given the following namespaces:
diff --git a/libs/checkpoint/langgraph/store/memory/__init__.py b/libs/checkpoint/langgraph/store/memory/__init__.py
index eec511ac8..d2786db48 100644
--- a/libs/checkpoint/langgraph/store/memory/__init__.py
+++ b/libs/checkpoint/langgraph/store/memory/__init__.py
@@ -1,31 +1,102 @@
-"""In-memory key-value store.
+"""In-memory dictionary-backed store with optional vector search.
-A lightweight store implementation using Python dictionaries. Supports basic
-key-value operations and vector search when configured with embeddings.
-
-Examples:
+!!! example "Examples"
Basic key-value storage:
- store = InMemoryStore()
- store.put(("users", "123"), "prefs", {"theme": "dark"})
- item = store.get(("users", "123"), "prefs")
+ ```python
+ from langgraph.store.memory import InMemoryStore
- Vector search with embeddings:
- from langchain_openai import OpenAIEmbeddings
- store = InMemoryStore(index={
+ store = InMemoryStore()
+ store.put(("users", "123"), "prefs", {"theme": "dark"})
+ item = store.get(("users", "123"), "prefs")
+ ```
+
+ Vector search using LangChain embeddings:
+ ```python
+ from langchain.embeddings import init_embeddings
+ from langgraph.store.memory import InMemoryStore
+
+ store = InMemoryStore(
+ index={
"dims": 1536,
- "embed": OpenAIEmbeddings(model="text-embedding-3-small"),
- })
+ "embed": init_embeddings("openai:text-embedding-3-small")
+ }
+ )
- # Store documents
- store.put(("docs",), "doc1", {"text": "Python tutorial"})
- store.put(("docs",), "doc2", {"text": "TypeScript guide"})
+ # Store documents
+ store.put(("docs",), "doc1", {"text": "Python tutorial"})
+ store.put(("docs",), "doc2", {"text": "TypeScript guide"})
- # Search by similarity
- results = store.search(("docs",), query="python programming")
+ # Search by similarity
+ results = store.search(("docs",), query="python programming")
+ ```
+ Vector search using OpenAI SDK directly:
+ ```python
+ from openai import OpenAI
+ from langgraph.store.memory import InMemoryStore
-Note:
- For production use cases requiring persistence, use a database-backed store instead.
+ client = OpenAI()
+
+ def embed_texts(texts: list[str]) -> list[list[float]]:
+ response = client.embeddings.create(
+ model="text-embedding-3-small",
+ input=texts
+ )
+ return [e.embedding for e in response.data]
+
+ store = InMemoryStore(
+ index={
+ "dims": 1536,
+ "embed": embed_texts
+ }
+ )
+
+ # Store documents
+ store.put(("docs",), "doc1", {"text": "Python tutorial"})
+ store.put(("docs",), "doc2", {"text": "TypeScript guide"})
+
+ # Search by similarity
+ results = store.search(("docs",), query="python programming")
+ ```
+
+ Async vector search using OpenAI SDK:
+ ```python
+ from openai import AsyncOpenAI
+ from langgraph.store.memory import InMemoryStore
+
+ client = AsyncOpenAI()
+
+ async def aembed_texts(texts: list[str]) -> list[list[float]]:
+ response = await client.embeddings.create(
+ model="text-embedding-3-small",
+ input=texts
+ )
+ return [e.embedding for e in response.data]
+
+ store = InMemoryStore(
+ index={
+ "dims": 1536,
+ "embed": aembed_texts
+ }
+ )
+
+ # Store documents
+ await store.aput(("docs",), "doc1", {"text": "Python tutorial"})
+ await store.aput(("docs",), "doc2", {"text": "TypeScript guide"})
+
+ # Search by similarity
+ results = await store.asearch(("docs",), query="python programming")
+ ```
+
+Warning:
+ This store keeps all data in memory. Data is lost when the process exits.
+ For persistence, use a database-backed store like PostgresStore.
+
+Tip:
+ For vector search, install numpy for better performance:
+ ```bash
+ pip install numpy
+ ```
"""
import asyncio
@@ -62,17 +133,18 @@ logger = logging.getLogger(__name__)
class InMemoryStore(BaseStore):
"""In-memory dictionary-backed store with optional vector search.
- Examples:
+ !!! example "Examples"
Basic key-value storage:
store = InMemoryStore()
store.put(("users", "123"), "prefs", {"theme": "dark"})
item = store.get(("users", "123"), "prefs")
Vector search with embeddings:
- from langchain_openai import OpenAIEmbeddings
+ from langchain.embeddings import init_embeddings
store = InMemoryStore(index={
"dims": 1536,
- "embed": OpenAIEmbeddings(model="text-embedding-3-small"),
+ "embed": init_embeddings("openai:text-embedding-3-small"),
+ "fields": ["text"],
})
# Store documents
diff --git a/libs/sdk-js/src/client.ts b/libs/sdk-js/src/client.ts
index 14284bcff..581145229 100644
--- a/libs/sdk-js/src/client.ts
+++ b/libs/sdk-js/src/client.ts
@@ -1206,6 +1206,7 @@ export class StoreClient extends BaseClient {
* @param options.filter Optional dictionary of key-value pairs to filter results.
* @param options.limit Maximum number of items to return (default is 10).
* @param options.offset Number of items to skip before returning results (default is 0).
+ * @param options.query Optional search query.
* @returns Promise
*/
async searchItems(
@@ -1214,6 +1215,7 @@ export class StoreClient extends BaseClient {
filter?: Record;
limit?: number;
offset?: number;
+ query?: string;
},
): Promise {
const payload = {
@@ -1221,6 +1223,7 @@ export class StoreClient extends BaseClient {
filter: options?.filter,
limit: options?.limit ?? 10,
offset: options?.offset ?? 0,
+ query: options?.query,
};
const response = await this.fetch(
diff --git a/libs/sdk-js/src/schema.ts b/libs/sdk-js/src/schema.ts
index f68e3d42f..259bcd539 100644
--- a/libs/sdk-js/src/schema.ts
+++ b/libs/sdk-js/src/schema.ts
@@ -264,11 +264,6 @@ export interface Checkpoint {
export interface ListNamespaceResponse {
namespaces: string[][];
}
-
-export interface SearchItemsResponse {
- items: Item[];
-}
-
export interface Item {
namespace: string[];
key: string;
@@ -276,3 +271,10 @@ export interface Item {
createdAt: string;
updatedAt: string;
}
+
+export interface SearchItem extends Item {
+ score?: number;
+}
+export interface SearchItemsResponse {
+ items: SearchItem[];
+}
From aca67107c14516aa5b0cfd750adcb4c15aae21f0 Mon Sep 17 00:00:00 2001
From: Phoenix Logan
Date: Tue, 3 Dec 2024 20:26:06 -0800
Subject: [PATCH 105/149] fix: make database saver classes inheritance-friendly
(#2615)
Replace hardcoded database saver class names with `cls` in
`from_conn_string` factory methods to improve subclassing support
## Changes
* Replaced direct class instantiations with `cls(conn)` in
`from_conn_string` classmethods across all database implementations
* Updated both synchronous and asynchronous variants for DuckDB,
PostgreSQL, and SQLite savers
## Why
This refactor makes the database saver classes more extensible by
following Python's convention of using `cls` in class methods. This
enables proper inheritance patterns where subclasses can reuse the
factory methods without needing to override them. Previously, the
hardcoded class names would always instantiate the parent class, even
when called from a subclass.
## Testing
The change is backward compatible and doesn't alter existing
functionality. All existing tests should continue to pass as this is
purely a structural refactoring that preserves the current behavior
while improving extensibility.
## Notes
This PR addresses follow up on comments from #2518 - AsyncPostgresSaver
didn't need to be fixed but many of the other DB saver classes did.
---
libs/checkpoint-duckdb/langgraph/checkpoint/duckdb/__init__.py | 2 +-
libs/checkpoint-duckdb/langgraph/checkpoint/duckdb/aio.py | 2 +-
libs/checkpoint-duckdb/langgraph/store/duckdb/aio.py | 2 +-
libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py | 2 +-
libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py | 2 +-
libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/libs/checkpoint-duckdb/langgraph/checkpoint/duckdb/__init__.py b/libs/checkpoint-duckdb/langgraph/checkpoint/duckdb/__init__.py
index 1002eebe8..7a873ab4b 100644
--- a/libs/checkpoint-duckdb/langgraph/checkpoint/duckdb/__init__.py
+++ b/libs/checkpoint-duckdb/langgraph/checkpoint/duckdb/__init__.py
@@ -42,7 +42,7 @@ class DuckDBSaver(BaseDuckDBSaver):
DuckDBSaver: A new DuckDBSaver instance.
"""
with duckdb.connect(conn_string) as conn:
- yield DuckDBSaver(conn)
+ yield cls(conn)
def setup(self) -> None:
"""Set up the checkpoint database asynchronously.
diff --git a/libs/checkpoint-duckdb/langgraph/checkpoint/duckdb/aio.py b/libs/checkpoint-duckdb/langgraph/checkpoint/duckdb/aio.py
index aa52feb18..54c1924cc 100644
--- a/libs/checkpoint-duckdb/langgraph/checkpoint/duckdb/aio.py
+++ b/libs/checkpoint-duckdb/langgraph/checkpoint/duckdb/aio.py
@@ -45,7 +45,7 @@ class AsyncDuckDBSaver(BaseDuckDBSaver):
AsyncDuckDBSaver: A new AsyncDuckDBSaver instance.
"""
with duckdb.connect(conn_string) as conn:
- yield AsyncDuckDBSaver(conn)
+ yield cls(conn)
async def setup(self) -> None:
"""Set up the checkpoint database asynchronously.
diff --git a/libs/checkpoint-duckdb/langgraph/store/duckdb/aio.py b/libs/checkpoint-duckdb/langgraph/store/duckdb/aio.py
index d6fd7dd89..f050f449b 100644
--- a/libs/checkpoint-duckdb/langgraph/store/duckdb/aio.py
+++ b/libs/checkpoint-duckdb/langgraph/store/duckdb/aio.py
@@ -156,7 +156,7 @@ class AsyncDuckDBStore(AsyncBatchedBaseStore, BaseDuckDBStore):
AsyncDuckDBStore: A new AsyncDuckDBStore instance.
"""
with duckdb.connect(conn_string) as conn:
- yield AsyncDuckDBStore(conn)
+ yield cls(conn)
async def setup(self) -> None:
"""Set up the store database asynchronously.
diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
index 4c0f5295c..3a1e13db1 100644
--- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
+++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
@@ -54,7 +54,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
pipeline: bool = False,
serde: Optional[SerializerProtocol] = None,
) -> AsyncIterator["AsyncPostgresSaver"]:
- """Create a new PostgresSaver instance from a connection string.
+ """Create a new AsyncPostgresSaver instance from a connection string.
Args:
conn_string (str): The Postgres connection info string.
diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py
index b552a75f4..ea749473c 100644
--- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py
+++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py
@@ -110,7 +110,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
check_same_thread=False,
)
) as conn:
- yield SqliteSaver(conn)
+ yield cls(conn)
def setup(self) -> None:
"""Set up the checkpoint database.
diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py
index 21cde06e0..72fca5bea 100644
--- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py
+++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py
@@ -137,7 +137,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
AsyncSqliteSaver: A new AsyncSqliteSaver instance.
"""
async with aiosqlite.connect(conn_string) as conn:
- yield AsyncSqliteSaver(conn)
+ yield cls(conn)
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
"""Get a checkpoint tuple from the database.
From 9b8bf70d9ebf5080c082c71f70fe914f599ca195 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Tue, 3 Dec 2024 20:36:59 -0800
Subject: [PATCH 106/149] Add link to local studio testing (#2617)
---
docs/docs/how-tos/index.md | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md
index f1c0f1a51..e579902d9 100644
--- a/docs/docs/how-tos/index.md
+++ b/docs/docs/how-tos/index.md
@@ -71,7 +71,7 @@ you to involve humans in the decision-making process of your graph. These how-to
### Tool calling
-[Tool calling](https://python.langchain.com/docs/concepts/tool_calling/) is a type of chat model API that accepts tool schemas, along with messages, as input and returns invocations of those tools as part of the output message.
+[Tool calling](https://python.langchain.com/docs/concepts/tool_calling/) is a type of chat model API that accepts tool schemas, along with messages, as input and returns invocations of those tools as part of the output message.
These how-to guides show common patterns for tool calling with LangGraph:
@@ -124,7 +124,7 @@ These guides show how to use the prebuilt ReAct agent:
This section includes how-to guides for LangGraph Platform.
-LangGraph Platform is a commercial solution for deploying agentic applications in production, built on the open-source LangGraph framework.
+LangGraph Platform is a commercial solution for deploying agentic applications in production, built on the open-source LangGraph framework.
The LangGraph Platform offers a few different deployment options described in the [deployment options guide](../concepts/deployment_options.md).
@@ -152,8 +152,8 @@ LangGraph applications can be deployed using LangGraph Cloud, which provides a r
- [How to deploy to LangGraph cloud](../cloud/deployment/cloud.md)
- [How to deploy to a self-hosted environment](./deploy-self-hosted.md)
-- [How to interact with the deployment using RemoteGraph](./use-remote-graph.md)
-
+- [How to interact with the deployment using RemoteGraph](./use-remote-graph.md)
+
### Assistants
[Assistants](../concepts/assistants.md) is a configured instance of a template.
@@ -198,7 +198,7 @@ When designing complex graphs, relying entirely on the LLM for decision-making c
### Double-texting
-Graph execution can take a while, and sometimes users may change their mind about the input they wanted to send before their original input has finished running. For example, a user might notice a typo in their original request and will edit the prompt and resend it. Deciding what to do in these cases is important for ensuring a smooth user experience and preventing your graphs from behaving in unexpected ways.
+Graph execution can take a while, and sometimes users may change their mind about the input they wanted to send before their original input has finished running. For example, a user might notice a typo in their original request and will edit the prompt and resend it. Deciding what to do in these cases is important for ensuring a smooth user experience and preventing your graphs from behaving in unexpected ways.
- [How to use the interrupt option](../cloud/how-tos/interrupt_concurrent.md)
- [How to use the rollback option](../cloud/how-tos/rollback_concurrent.md)
@@ -218,8 +218,9 @@ Graph execution can take a while, and sometimes users may change their mind abou
LangGraph Studio is a built-in UI for visualizing, testing, and debugging your agents.
- [How to connect to a LangGraph Cloud deployment](../cloud/how-tos/test_deployment.md)
-- [How to connect to a local deployment](../cloud/how-tos/test_local_deployment.md)
-- [How to test your graph in LangGraph Studio](../cloud/how-tos/invoke_studio.md)
+- [How to connect to a local dev server](../how-tos/local-studio.md)
+- [How to connect to a local deployment (Docker)](../cloud/how-tos/test_local_deployment.md)
+- [How to test your graph in LangGraph Studio (MacOS only)](../cloud/how-tos/invoke_studio.md)
- [How to interact with threads in LangGraph Studio](../cloud/how-tos/threads_studio.md)
## Troubleshooting
@@ -231,5 +232,3 @@ These are the guides for resolving common errors you may find while building wit
- [INVALID_GRAPH_NODE_RETURN_VALUE](../troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md)
- [MULTIPLE_SUBGRAPHS](../troubleshooting/errors/MULTIPLE_SUBGRAPHS.md)
- [INVALID_CHAT_HISTORY](../troubleshooting/errors/INVALID_CHAT_HISTORY.md)
-
-
From 879df6b52c124c8b0431b348b8939fecde9efadd Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Tue, 3 Dec 2024 23:01:19 -0800
Subject: [PATCH 107/149] [JS] Update SDK version (#2619)
---
libs/sdk-js/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/sdk-js/package.json b/libs/sdk-js/package.json
index 3b373dabe..02628e6dc 100644
--- a/libs/sdk-js/package.json
+++ b/libs/sdk-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
- "version": "0.0.30",
+ "version": "0.0.31",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
From 9220049b35d9900885abb7e2f9782c8112882884 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 4 Dec 2024 06:28:54 -0800
Subject: [PATCH 108/149] Add store langgraph.json config ref (#2622)
---
docs/docs/cloud/reference/cli.md | 117 +++++++++++++++++++++----------
1 file changed, 80 insertions(+), 37 deletions(-)
diff --git a/docs/docs/cloud/reference/cli.md b/docs/docs/cloud/reference/cli.md
index 0db84cb43..34629fa4e 100644
--- a/docs/docs/cloud/reference/cli.md
+++ b/docs/docs/cloud/reference/cli.md
@@ -26,10 +26,11 @@ The LangGraph command line interface includes commands to build and run a LangGr
The LangGraph CLI requires a JSON configuration file with the following keys:
| Key | Description |
-|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dependencies` | **Required**. Array of dependencies for LangGraph Cloud API server. Dependencies can be one of the following: (1) `"."`, which will look for local Python packages, (2) `pyproject.toml`, `setup.py` or `requirements.txt` in the app directory `"./local_package"`, or (3) a package name. |
| `graphs` | **Required**. Mapping from graph ID to path where the compiled graph or a function that makes a graph is defined. Example: - `./your_package/your_file.py:variable`, where `variable` is an instance of `langgraph.graph.state.CompiledStateGraph`
- `./your_package/your_file.py:make_graph`, where `make_graph` is a function that takes a config dictionary (`langchain_core.runnables.RunnableConfig`) and creates an instance of `langgraph.graph.state.StateGraph` / `langgraph.graph.state.CompiledStateGraph`.
|
| `env` | Path to `.env` file or a mapping from environment variable to its value. |
+| `store` | Configuration for adding semantic search to the BaseStore. Contains the following fields: - `index`: Configuration for semantic search indexing with fields:
- `embed`: Embedding provider (e.g., "openai:text-embedding-3-small") or path to custom embedding function
- `dims`: Dimension size of the embedding model. Used to initialize the vector table.
- `fields` (optional): List of fields to index. Defaults to `["$"]`, meaningto index entire documents. Can be specific fields like `["text", "summary", "some.value"]`
|
| `python_version` | `3.11` or `3.12`. Defaults to `3.11`. |
| `pip_config_file` | Path to `pip` config file. |
| `dockerfile_lines` | Array of additional lines to add to Dockerfile following the import from parent image. |
@@ -41,33 +42,75 @@ The LangGraph CLI requires a JSON configuration file with the following keys:
-Example:
+### Examples
+
+#### Basic Configuration
```json
{
- "dependencies": ["langchain_openai", "./your_package"],
+ "dependencies": ["."],
"graphs": {
- "my_graph_id": "./your_package/your_file.py:variable"
- },
- "env": "./.env"
+ "chat": "./chat/graph.py:graph"
+ }
}
```
-Example with environment variables:
+#### Adding semantic search to the store
+
+All deployments come with a DB-backed BaseStore. Adding an "index" configuration to your `langgraph.json` will enable [semantic search](../deployment/semantic_search.md) within the BaseStore of your deployment.
+
+The `fields` configuration determines which parts of your documents to embed:
+- If omitted or set to `["$"]`, the entire document will be embedded
+- To embed specific fields, use JSON path notation: `["metadata.title", "content.text"]`
+- Documents missing specified fields will still be stored but won't have embeddings for those fields
+- You can still override which fields to embed on a specific item at `put` time using the `index` parameter
```json
{
- "python_version": "3.11",
- "dependencies": ["langchain_openai", "."],
+ "dependencies": ["."],
"graphs": {
- "my_graph_id": "./your_package/your_file.py:make_graph"
+ "memory_agent": "./agent/graph.py:graph"
},
- "env": {
- "OPENAI_API_KEY": "secret-key"
+ "store": {
+ "index": {
+ "embed": "openai:text-embedding-3-small",
+ "dims": 1536,
+ "fields": ["$"]
+ }
}
}
```
+#### Semantic search with a custom embedding function
+
+If you want to use semantic search with a custom embedding function, you can pass a path to a custom embedding function:
+
+```json
+{
+ "dependencies": ["."],
+ "graphs": {
+ "memory_agent": "./agent/graph.py:graph"
+ },
+ "store": {
+ "index": {
+ "embed": "./embeddings.py:embed_texts",
+ "dims": 768,
+ "fields": ["text", "summary"]
+ }
+ }
+}
+```
+
+The `embed` field in store configuration can reference a custom function that takes a list of strings and returns a list of embeddings. Example implementation:
+
+```python
+# embeddings.py
+def embed_texts(texts: list[str]) -> list[list[float]]:
+ """Custom embedding function for semantic search."""
+ # Implementation using your preferred embedding model
+ return [[0.1, 0.2, ...] for _ in texts] # dims-dimensional vectors
+```
+
## Commands
The base command for the LangGraph CLI is `langgraph`.
@@ -98,16 +141,16 @@ langgraph dev [OPTIONS]
**Options**
-| Option | Default | Description |
-|----------------------------|------------------|--------------------------------------------------------------------------------------------|
-| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables |
-| `--host TEXT` | `127.0.0.1` | Host to bind the server to |
-| `--port INTEGER` | `2024` | Port to bind the server to |
-| `--no-reload` | | Disable auto-reload |
-| `--n-jobs-per-worker INTEGER` | | Number of jobs per worker. Default is 10 |
-| `--no-browser` | | Disable automatic browser opening |
-| `--debug-port INTEGER` | | Port for debugger to listen on |
-| `--help` | | Display command documentation |
+| Option | Default | Description |
+| ----------------------------- | ---------------- | ----------------------------------------------------------------------------------- |
+| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables |
+| `--host TEXT` | `127.0.0.1` | Host to bind the server to |
+| `--port INTEGER` | `2024` | Port to bind the server to |
+| `--no-reload` | | Disable auto-reload |
+| `--n-jobs-per-worker INTEGER` | | Number of jobs per worker. Default is 10 |
+| `--no-browser` | | Disable automatic browser opening |
+| `--debug-port INTEGER` | | Port for debugger to listen on |
+| `--help` | | Display command documentation |
### `build`
@@ -122,7 +165,7 @@ langgraph build [OPTIONS]
**Options**
| Option | Default | Description |
-|----------------------|------------------|------------------------------------------------------------------------------------------------------------------------------|
+| -------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `--platform TEXT` | | Target platform(s) to build the Docker image for. Example: `langgraph build --platform linux/amd64,linux/arm64` |
| `-t, --tag TEXT` | | **Required**. Tag for the Docker image. Example: `langgraph build -t my-image` |
| `--pull / --no-pull` | `--pull` | Build with latest remote Docker image. Use `--no-pull` for running the LangGraph Cloud API server with locally built images. |
@@ -141,20 +184,20 @@ langgraph up [OPTIONS]
**Options**
-| Option | Default | Description |
-|------------------------------|---------------------------|-----------------------------------------------------------------------------------------------------------------------|
-| `--wait` | | Wait for services to start before returning. Implies --detach |
-| `--postgres-uri TEXT` | Local database | Postgres URI to use for the database. |
-| `--watch` | | Restart on file changes |
-| `--debugger-base-url TEXT` | `http://127.0.0.1:[PORT]` | URL used by the debugger to access LangGraph API. |
-| `--debugger-port INTEGER` | | Pull the debugger image locally and serve the UI on specified port |
-| `--verbose` | | Show more output from the server logs. |
-| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
-| `-d, --docker-compose FILE` | | Path to docker-compose.yml file with additional services to launch. |
-| `-p, --port INTEGER` | `8123` | Port to expose. Example: `langgraph up --port 8000` |
+| Option | Default | Description |
+| ---------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
+| `--wait` | | Wait for services to start before returning. Implies --detach |
+| `--postgres-uri TEXT` | Local database | Postgres URI to use for the database. |
+| `--watch` | | Restart on file changes |
+| `--debugger-base-url TEXT` | `http://127.0.0.1:[PORT]` | URL used by the debugger to access LangGraph API. |
+| `--debugger-port INTEGER` | | Pull the debugger image locally and serve the UI on specified port |
+| `--verbose` | | Show more output from the server logs. |
+| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
+| `-d, --docker-compose FILE` | | Path to docker-compose.yml file with additional services to launch. |
+| `-p, --port INTEGER` | `8123` | Port to expose. Example: `langgraph up --port 8000` |
| `--pull / --no-pull` | `pull` | Pull latest images. Use `--no-pull` for running the server with locally-built images. Example: `langgraph up --no-pull` |
-| `--recreate / --no-recreate` | `no-recreate` | Recreate containers even if their configuration and image haven't changed |
-| `--help` | | Display command documentation. |
+| `--recreate / --no-recreate` | `no-recreate` | Recreate containers even if their configuration and image haven't changed |
+| `--help` | | Display command documentation. |
### `dockerfile`
@@ -169,7 +212,7 @@ langgraph dockerfile [OPTIONS] SAVE_PATH
**Options**
| Option | Default | Description |
-|---------------------|------------------|-----------------------------------------------------------------------------------------------------------------|
+| ------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------- |
| `-c, --config FILE` | `langgraph.json` | Path to the [configuration file](#configuration-file) declaring dependencies, graphs and environment variables. |
| `--help` | | Show this message and exit. |
From 84d33f9621dbdc8b7ee76b371da40d7fded060d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=B9=9B=E9=9C=B2=E5=85=88=E7=94=9F?=
Date: Wed, 4 Dec 2024 22:29:28 +0800
Subject: [PATCH 109/149] Fix typos in langgraph_sdk client. (#2621)
Fix typos in langgraph_sdk client.
Signed-off-by: zhanluxianshen
---
libs/sdk-py/langgraph_sdk/client.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py
index 3c570df2a..c453eb749 100644
--- a/libs/sdk-py/langgraph_sdk/client.py
+++ b/libs/sdk-py/langgraph_sdk/client.py
@@ -190,7 +190,7 @@ class LangGraphClient:
class HttpClient:
- """Hancle async requests to the LangGraph API.
+ """Handle async requests to the LangGraph API.
Adds additional error messaging & content handling above the
provided httpx client.
From a8db511e2469ece19823c8bceaa4fdd062d343c6 Mon Sep 17 00:00:00 2001
From: ACMCMC <20495460+ACMCMC@users.noreply.github.com>
Date: Wed, 4 Dec 2024 14:30:05 +0000
Subject: [PATCH 110/149] Fix typo (#2620)
---
docs/docs/concepts/human_in_the_loop.md | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/docs/docs/concepts/human_in_the_loop.md b/docs/docs/concepts/human_in_the_loop.md
index 8728d4ba5..45ce792d4 100644
--- a/docs/docs/concepts/human_in_the_loop.md
+++ b/docs/docs/concepts/human_in_the_loop.md
@@ -27,8 +27,8 @@ Adding a [breakpoint](./low_level.md#breakpoints) a specific location in the gra
Here, we compile our graph with a checkpointer and a breakpoint at the node we want to interrupt before, `step_for_human_in_the_loop`. We then perform one of the above interaction patterns, which will create a new checkpoint if a human edits the graph state. The new checkpoint is saved to the `thread` and we can resume the graph execution from there by passing in `None` as the input.
```python
-# Compile our graph with a checkpoitner and a breakpoint before "step_for_human_in_the_loop"
-graph = builder.compile(checkpointer=checkpoitner, interrupt_before=["step_for_human_in_the_loop"])
+# Compile our graph with a checkpointer and a breakpoint before "step_for_human_in_the_loop"
+graph = builder.compile(checkpointer=checkpointer, interrupt_before=["step_for_human_in_the_loop"])
# Run the graph up to the breakpoint
thread_config = {"configurable": {"thread_id": "1"}}
@@ -98,8 +98,8 @@ With persistence, we can surface the current agent state as well as the next ste
If approved, the graph resumes execution from the last saved checkpoint, which is saved to the `thread`:
```python
-# Compile our graph with a checkpoitner and a breakpoint before the step to approve
-graph = builder.compile(checkpointer=checkpoitner, interrupt_before=["node_2"])
+# Compile our graph with a checkpointer and a breakpoint before the step to approve
+graph = builder.compile(checkpointer=checkpointer, interrupt_before=["node_2"])
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
@@ -131,8 +131,8 @@ We can edit the graph state by forking the current checkpoint, which is saved to
We can then proceed with the graph from our forked checkpoint as done before.
```python
-# Compile our graph with a checkpoitner and a breakpoint before the step to review
-graph = builder.compile(checkpointer=checkpoitner, interrupt_before=["node_2"])
+# Compile our graph with a checkpointer and a breakpoint before the step to review
+graph = builder.compile(checkpointer=checkpointer, interrupt_before=["node_2"])
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
@@ -173,8 +173,8 @@ With input, we explicitly define a node in our graph for collecting human input!
The state update with the human input then runs *as this node*.
```python
-# Compile our graph with a checkpoitner and a breakpoint before the step to to collect human input
-graph = builder.compile(checkpointer=checkpoitner, interrupt_before=["human_input"])
+# Compile our graph with a checkpointer and a breakpoint before the step to to collect human input
+graph = builder.compile(checkpointer=checkpointer, interrupt_before=["human_input"])
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
@@ -211,8 +211,8 @@ Even if the tool call is correct, we may also want to apply discretion:
With these points in mind, we can combine the above ideas to create a human-in-the-loop review of a tool call.
```python
-# Compile our graph with a checkpoitner and a breakpoint before the step to to review the tool call from the LLM
-graph = builder.compile(checkpointer=checkpoitner, interrupt_before=["human_review"])
+# Compile our graph with a checkpointer and a breakpoint before the step to to review the tool call from the LLM
+graph = builder.compile(checkpointer=checkpointer, interrupt_before=["human_review"])
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
@@ -319,4 +319,4 @@ for event in graph.stream(None, config, stream_mode="values"):
See [this additional conceptual guide](https://langchain-ai.github.io/langgraph/concepts/persistence/#update-state) for related context on forking.
-See see [this guide](../how-tos/human_in_the_loop/time-travel.ipynb) for a detailed how-to on doing time-travel!
\ No newline at end of file
+See see [this guide](../how-tos/human_in_the_loop/time-travel.ipynb) for a detailed how-to on doing time-travel!
From e6c83abecd2e14f5d0f551ba40180746ce434b29 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 4 Dec 2024 06:55:15 -0800
Subject: [PATCH 111/149] Fix ref doc formatting (#2623)
---
Makefile | 2 +-
.../langgraph/store/base/__init__.py | 111 ++++++++++++------
2 files changed, 73 insertions(+), 40 deletions(-)
diff --git a/Makefile b/Makefile
index 0be07e405..0d039591e 100644
--- a/Makefile
+++ b/Makefile
@@ -13,7 +13,7 @@ serve-clean-docs: clean-docs
poetry run python -m mkdocs serve -c -f docs/mkdocs.yml --strict -w ./libs/langgraph
serve-docs: build-typedoc
- poetry run python -m mkdocs serve -f docs/mkdocs.yml -w ./libs/langgraph --dirty
+ poetry run python -m mkdocs serve -f docs/mkdocs.yml -w ./libs/langgraph -w ./libs/checkpoint --dirty
clean-docs:
find ./docs/docs -name "*.ipynb" -type f -delete
diff --git a/libs/checkpoint/langgraph/store/base/__init__.py b/libs/checkpoint/langgraph/store/base/__init__.py
index 9673fc6cb..d2fd004ec 100644
--- a/libs/checkpoint/langgraph/store/base/__init__.py
+++ b/libs/checkpoint/langgraph/store/base/__init__.py
@@ -133,7 +133,7 @@ class GetOp(NamedTuple):
This operation allows precise retrieval of stored items using their full path
(namespace) and unique identifier (key) combination.
- ???+example "Examples"
+ ???+ example "Examples"
Basic item retrieval:
```python
@@ -145,7 +145,7 @@ class GetOp(NamedTuple):
namespace: tuple[str, ...]
"""Hierarchical path that uniquely identifies the item's location.
- ???+example "Examples"
+ ???+ example "Examples"
```python
("users",) # Root level users namespace
@@ -156,7 +156,7 @@ class GetOp(NamedTuple):
key: str
"""Unique identifier for the item within its specific namespace.
- ???+example "Examples"
+ ???+ example "Examples"
```python
"user123" # For a user profile
@@ -175,7 +175,7 @@ class SearchOp(NamedTuple):
Note:
Natural language search support depends on your store implementation.
- ???+example "Examples"
+ ???+ example "Examples"
Search with filters and pagination:
```python
SearchOp(
@@ -199,7 +199,7 @@ class SearchOp(NamedTuple):
namespace_prefix: tuple[str, ...]
"""Hierarchical path prefix defining the search scope.
- ???+example "Examples"
+ ???+ example "Examples"
```python
() # Search entire store
@@ -221,7 +221,7 @@ class SearchOp(NamedTuple):
- $lt: Less than
- $lte: Less than or equal to
- ???+example "Examples"
+ ???+ example "Examples"
Simple exact match:
```python
@@ -253,7 +253,7 @@ class SearchOp(NamedTuple):
query: Optional[str] = None
"""Natural language search query for semantic search capabilities.
- ???+example "Examples"
+ ???+ example "Examples"
- "technical documentation about REST APIs"
- "machine learning papers from 2023"
"""
@@ -263,7 +263,7 @@ class SearchOp(NamedTuple):
NamespacePath = tuple[Union[str, Literal["*"]], ...]
"""A tuple representing a namespace path that can include wildcards.
-???+example "Examples"
+???+ example "Examples"
```python
("users",) # Exact users namespace
("documents", "*") # Any sub-namespace under documents
@@ -288,7 +288,7 @@ class MatchCondition(NamedTuple):
pattern that can include wildcards to flexibly match different namespace
hierarchies.
- ???+example "Examples"
+ ???+ example "Examples"
Prefix matching:
```python
MatchCondition(match_type="prefix", path=("users", "profiles"))
@@ -318,7 +318,7 @@ class ListNamespacesOp(NamedTuple):
This operation allows exploring the organization of data, finding specific
collections, and navigating the namespace hierarchy.
- ???+example "Examples"
+ ???+ example "Examples"
List all namespaces under the "documents" path:
```python
@@ -341,7 +341,7 @@ class ListNamespacesOp(NamedTuple):
match_conditions: Optional[tuple[MatchCondition, ...]] = None
"""Optional conditions for filtering namespaces.
- ???+example "Examples"
+ ???+ example "Examples"
All user namespaces:
```python
(MatchCondition(match_type="prefix", path=("users",)),)
@@ -383,7 +383,7 @@ class PutOp(NamedTuple):
The namespace acts as a folder-like structure to organize items.
Each element in the tuple represents one level in the hierarchy.
- ???+example "Examples"
+ ???+ example "Examples"
Root level documents
```python
("documents",)
@@ -429,9 +429,9 @@ class PutOp(NamedTuple):
"""Controls how the item's fields are indexed for search operations.
Indexing configuration determines how the item can be found through search:
- - None (default): Uses the store's default indexing configuration (if provided)
- - False: Disables indexing for this item
- - list[str]: Specifies which json path fields to index for search
+ - None (default): Uses the store's default indexing configuration (if provided)
+ - False: Disables indexing for this item
+ - list[str]: Specifies which json path fields to index for search
The item remains accessible through direct get() operations regardless of indexing.
When indexed, fields can be searched using natural language queries through
@@ -445,15 +445,14 @@ class PutOp(NamedTuple):
- Last element: "array[-1]"
- All elements (each individually): "array[*]"
- ???+example "Examples"
- - None - Use store defaults
- - False - Don't index this item
+ ???+ example "Examples"
+ - None - Use store defaults (whole item)
- list[str] - List of fields to index
```python
[
"metadata.title", # Nested field access
- "chapters[*].content", # Index content from all chapters as separate vectors
+ "context[*].content", # Index content from all context as separate vectors
"authors[0].name", # First author's name
"revisions[-1].changes", # Most recent revision's changes
"sections[*].paragraphs[*].text", # All text from all paragraphs in all sections
@@ -495,7 +494,7 @@ class IndexConfig(TypedDict, total=False):
2. A synchronous embedding function (EmbeddingsFunc)
3. An asynchronous embedding function (AEmbeddingsFunc)
- ???+example "Examples"
+ ???+ example "Examples"
Using LangChain's initialization with InMemoryStore:
```python
from langchain.embeddings import init_embeddings
@@ -557,7 +556,32 @@ class IndexConfig(TypedDict, total=False):
fields: Optional[list[str]]
"""Fields to extract text from for embedding generation.
- Defaults to the root ["$"], which embeds the json object as a whole.
+ Controls which parts of stored items are embedded for semantic search. Follows JSON path syntax:
+ - ["$"] (default): Embeds the entire JSON object as one vector
+ - ["field1", "field2"]: Embeds specific top-level fields
+ - ["parent.child"]: Embeds nested fields using dot notation
+ - ["array[*].field"]: Embeds field from each array element separately
+
+ ???+ example "Examples"
+ ```python
+ # Embed entire document (default)
+ fields=["$"]
+
+ # Embed specific fields
+ fields=["text", "summary"]
+
+ # Embed nested fields
+ fields=["metadata.title", "content.body"]
+
+ # Embed from arrays
+ fields=["messages[*].content"] # Each message content separately
+ fields=["context[0].text"] # First context item's text
+ ```
+
+ Note:
+ - Fields missing from a document are skipped
+ - Array notation creates separate embeddings for each element
+ - Complex nested paths are supported (e.g., "a.b[*].c.d")
"""
@@ -645,7 +669,7 @@ class BaseStore(ABC):
index={
"dims": 1536, # embedding dimensions
"embed": your_embedding_function, # function to create embeddings
- "fields": ["text"] # fields to embed
+ "fields": ["text"] # fields to embed. Defaults to ["$"]
}
)
@@ -680,7 +704,8 @@ class BaseStore(ABC):
value: Dictionary containing the item's data. Must contain string keys
and JSON-serializable values.
index: Controls how the item's fields are indexed for search:
- - None (default): Use store's default indexing configuration
+
+ - None (default): Use `fields` you configured when creating the store (if any)
- False: Disable indexing for this item
- list[str]: List of field paths to index, supporting:
- Nested fields: "metadata.title"
@@ -691,20 +716,21 @@ class BaseStore(ABC):
Indexing capabilities depend on your store implementation.
Some implementations may support only a subset of indexing features.
- ???+example "Examples"
- Simple storage without special indexing (respects store defaults)
+ ???+ example "Examples"
+ Store item. Indexing depends on how you configure the store.
```python
- store.put(("docs",), "report", {"title": "Annual Report"})
+ store.put(("docs",), "report", {"memory": "Will likes ai"})
```
- Index specific fields for search (if store configured to index items)
+ Do not index item for semantic search. Still accessible through get()
+ and search() operations but won't have a vector representation.
```python
- store.put(("docs",), "report", {"title": "Annual Report"}, index=["title"])
+ store.put(("docs",), "report", {"memory": "Will likes ai"}, index=False)
```
- Do not index for semantic search
+ Index specific fields for search.
```python
- store.put(("docs",), "report", {"title": "Annual Report"}, index=False)
+ store.put(("docs",), "report", {"memory": "Will likes ai"}, index=["memory"])
```
"""
_validate_namespace(namespace)
@@ -745,7 +771,7 @@ class BaseStore(ABC):
List[Tuple[str, ...]]: A list of namespace tuples that match the criteria.
Each tuple represents a full namespace path up to `max_depth`.
- ???+example "Examples":
+ ???+ example "Examples":
Setting max_depth=3. Given the namespaces:
```python
# Example if you have the following namespaces:
@@ -862,7 +888,8 @@ class BaseStore(ABC):
value: Dictionary containing the item's data. Must contain string keys
and JSON-serializable values.
index: Controls how the item's fields are indexed for search:
- - None (default): Use store's default indexing configuration
+
+ - None (default): Use `fields` you configured when creating the store (if any)
- False: Disable indexing for this item
- list[str]: List of field paths to index, supporting:
- Nested fields: "metadata.title"
@@ -873,10 +900,16 @@ class BaseStore(ABC):
Indexing capabilities depend on your store implementation.
Some implementations may support only a subset of indexing features.
- ???+example "Examples"
- Simple storage without special indexing:
+ ???+ example "Examples"
+ Store item. Indexing depends on how you configure the store.
```python
- await store.aput(("docs",), "report", {"title": "Annual Report"})
+ await store.aput(("docs",), "report", {"memory": "Will likes ai"})
+ ```
+
+ Do not index item for semantic search. Still accessible through get()
+ and search() operations but won't have a vector representation.
+ ```python
+ await store.aput(("docs",), "report", {"memory": "Will likes ai"}, index=False)
```
Index specific fields for search (if store configured to index items):
@@ -885,10 +918,10 @@ class BaseStore(ABC):
("docs",),
"report",
{
- "title": "Q4 Report",
- "chapters": [{"content": "..."}, {"content": "..."}]
+ "memory": "Will likes ai",
+ "context": [{"content": "..."}, {"content": "..."}]
},
- index=["title", "chapters[*].content"]
+ index=["memory", "context[*].content"]
)
```
"""
@@ -930,7 +963,7 @@ class BaseStore(ABC):
List[Tuple[str, ...]]: A list of namespace tuples that match the criteria.
Each tuple represents a full namespace path up to `max_depth`.
- ???+example "Examples"
+ ???+ example "Examples"
Setting max_depth=3 with existing namespaces:
```python
# Given the following namespaces:
From c322f7ffa6b4cdfa6694eea6c717d585f026a5cd Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 4 Dec 2024 07:19:49 -0800
Subject: [PATCH 112/149] Add Memory Store conceptual doc section (#2624)
On semantic search
---
docs/docs/cloud/deployment/semantic_search.md | 2 +-
docs/docs/cloud/reference/cli.md | 9 +++
docs/docs/concepts/persistence.md | 70 +++++++++++++++++--
.../langgraph/store/base/__init__.py | 14 ++--
libs/cli/langgraph_cli/config.py | 14 ++--
5 files changed, 87 insertions(+), 22 deletions(-)
diff --git a/docs/docs/cloud/deployment/semantic_search.md b/docs/docs/cloud/deployment/semantic_search.md
index 918d0e3df..1ea48be13 100644
--- a/docs/docs/cloud/deployment/semantic_search.md
+++ b/docs/docs/cloud/deployment/semantic_search.md
@@ -59,7 +59,7 @@ def search_memory(state: State, *, store: BaseStore):
results = store.search(
namespace=("memory", "facts"), # Organize memories by type
query="your search query",
- k=3 # number of results to return
+ limit=3 # number of results to return
)
return results
```
diff --git a/docs/docs/cloud/reference/cli.md b/docs/docs/cloud/reference/cli.md
index 34629fa4e..914611ffe 100644
--- a/docs/docs/cloud/reference/cli.md
+++ b/docs/docs/cloud/reference/cli.md
@@ -81,6 +81,15 @@ The `fields` configuration determines which parts of your documents to embed:
}
```
+!!! note "Common model dimensions"
+ - openai:text-embedding-3-large: 3072
+ - openai:text-embedding-3-small: 1536
+ - openai:text-embedding-ada-002: 1536
+ - cohere:embed-english-v3.0: 1024
+ - cohere:embed-english-light-v3.0: 384
+ - cohere:embed-multilingual-v3.0: 1024
+ - cohere:embed-multilingual-light-v3.0: 384
+
#### Semantic search with a custom embedding function
If you want to use semantic search with a custom embedding function, you can pass a path to a custom embedding function:
diff --git a/docs/docs/concepts/persistence.md b/docs/docs/concepts/persistence.md
index c6ad10a12..6637fbee0 100644
--- a/docs/docs/concepts/persistence.md
+++ b/docs/docs/concepts/persistence.md
@@ -218,13 +218,16 @@ The final thing you can optionally specify when calling `update_state` is `as_no
## Memory Store
-
+
A [state schema](low_level.md#schema) specifies a set of keys that are populated as a graph is executed. As discussed above, state can be written by a checkpointer to a thread at each graph step, enabling state persistence.
But, what if we want to retrain some information *across threads*? Consider the case of a chatbot where we want to retain specific information about the user across *all* chat conversations (e.g., threads) with that user!
-With checkpointers alone, we cannot share information across threads. This motivates the need for the `Store` interface. As an illustration, we can define an `InMemoryStore` to store information about a user across threads. We simply compile our graph with a checkpointer, as before, and with our new `in_memory_store` variable.
+With checkpointers alone, we cannot share information across threads. This motivates the need for the [`Store`](../reference/store.md#langgraph.store.base.BaseStore) interface. As an illustration, we can define an `InMemoryStore` to store information about a user across threads. We simply compile our graph with a checkpointer, as before, and with our new `in_memory_store` variable.
+
+### Basic Usage
+
First, let's showcase this in isolation without using LangGraph.
```python
@@ -268,6 +271,56 @@ The attributes it has are:
- `created_at`: Timestamp for when this memory was created
- `updated_at`: Timestamp for when this memory was updated
+### Semantic Search
+
+Beyond simple retrieval, the store also supports semantic search, allowing you to find memories based on meaning rather than exact matches. To enable this, configure the store with an embedding model:
+
+```python
+store = InMemoryStore(
+ index={
+ "embed": "openai:text-embedding-3-small", # Embedding provider
+ "dims": 1536, # Embedding dimensions
+ "fields": ["food_preference", "$"] # Fields to embed
+ }
+)
+```
+
+Now when searching, you can use natural language queries to find relevant memories:
+
+```python
+# Find memories about food preferences
+memories = store.search(
+ namespace_for_memory,
+ query="What does the user like to eat?",
+ limit=3 # Return top 3 matches
+)
+```
+
+You can control which parts of your memories get embedded by configuring the `fields` parameter or by specifying the `index` parameter when storing memories:
+
+```python
+# Store with specific fields to embed
+store.put(
+ namespace_for_memory,
+ str(uuid.uuid4()),
+ {
+ "food_preference": "I love Italian cuisine",
+ "context": "Discussing dinner plans"
+ },
+ index=["food_preference"] # Only embed "food_preferences" field
+)
+
+# Store without embedding (still retrievable, but not searchable)
+store.put(
+ namespace_for_memory,
+ str(uuid.uuid4()),
+ {"system_info": "Last updated: 2024-01-01"},
+ index=False
+)
+```
+
+### Using in LangGraph
+
With this all in place, we use the `in_memory_store` in LangGraph. The `in_memory_store` works hand-in-hand with the checkpointer: the checkpointer saves state to threads, as discussed above, and the `in_memory_store` allows us to store arbitrary information for access *across* threads. We compile the graph with both the checkpointer and the `in_memory_store` as follows.
```python
@@ -296,7 +349,7 @@ for update in graph.stream(
print(update)
```
-We can access the `in_memory_store` and the `user_id` in *any node* by passing `store: BaseStore` and `config: RunnableConfig` as node arguments. Just as we saw above, simply use the `put` method to save memories to the store.
+We can access the `in_memory_store` and the `user_id` in *any node* by passing `store: BaseStore` and `config: RunnableConfig` as node arguments. Here's how we might use semantic search in a node to find relevant memories:
```python
def update_memory(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
@@ -332,12 +385,15 @@ We can access the memories and use them in our model call.
```python
def call_model(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
-
# Get the user id from the config
user_id = config["configurable"]["user_id"]
- # Get the memories for the user from the store
- memories = store.search(("memories", user_id))
+ # Search based on the most recent message
+ memories = store.search(
+ namespace,
+ query=state["messages"][-1].content,
+ limit=3
+ )
info = "\n".join([d.value["memory"] for d in memories])
# ... Use memories in the model call
@@ -356,7 +412,7 @@ for update in graph.stream(
print(update)
```
-When we use the LangGraph API, either locally (e.g., in LangGraph Studio) or with LangGraph Cloud, the memory store is available to use by default and does not need to be specified during graph compilation.
+When we use the LangGraph API, either locally (e.g., in LangGraph Studio) or with LangGraph Cloud, the base store is available to use by default and does not need to be specified during graph compilation. For cloud deployments, semantic search is automatically configured based on your `langgraph.json` settings. See the [deployment guide](../deployment/semantic_search.md) for more details.
## Checkpointer libraries
diff --git a/libs/checkpoint/langgraph/store/base/__init__.py b/libs/checkpoint/langgraph/store/base/__init__.py
index d2fd004ec..3f36b7f16 100644
--- a/libs/checkpoint/langgraph/store/base/__init__.py
+++ b/libs/checkpoint/langgraph/store/base/__init__.py
@@ -477,13 +477,13 @@ class IndexConfig(TypedDict, total=False):
"""Number of dimensions in the embedding vectors.
Common embedding models have the following dimensions:
- - OpenAI text-embedding-3-large: 256, 1024, or 3072
- - OpenAI text-embedding-3-small: 512 or 1536
- - OpenAI text-embedding-ada-002: 1536
- - Cohere embed-english-v3.0: 1024
- - Cohere embed-english-light-v3.0: 384
- - Cohere embed-multilingual-v3.0: 1024
- - Cohere embed-multilingual-light-v3.0: 384
+ - openai:text-embedding-3-large: 3072
+ - openai:text-embedding-3-small: 1536
+ - openai:text-embedding-ada-002: 1536
+ - cohere:embed-english-v3.0: 1024
+ - cohere:embed-english-light-v3.0: 384
+ - cohere:embed-multilingual-v3.0: 1024
+ - cohere:embed-multilingual-light-v3.0: 384
"""
embed: Union[Embeddings, EmbeddingsFunc, AEmbeddingsFunc]
diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py
index 99db440e1..5a15ac479 100644
--- a/libs/cli/langgraph_cli/config.py
+++ b/libs/cli/langgraph_cli/config.py
@@ -17,13 +17,13 @@ class IndexConfig(TypedDict, total=False):
"""Number of dimensions in the embedding vectors.
Common embedding models have the following dimensions:
- - OpenAI text-embedding-3-large: 256, 1024, or 3072
- - OpenAI text-embedding-3-small: 512 or 1536
- - OpenAI text-embedding-ada-002: 1536
- - Cohere embed-english-v3.0: 1024
- - Cohere embed-english-light-v3.0: 384
- - Cohere embed-multilingual-v3.0: 1024
- - Cohere embed-multilingual-light-v3.0: 384
+ - openai:text-embedding-3-large: 3072
+ - openai:text-embedding-3-small: 1536
+ - openai:text-embedding-ada-002: 1536
+ - cohere:embed-english-v3.0: 1024
+ - cohere:embed-english-light-v3.0: 384
+ - cohere:embed-multilingual-v3.0: 1024
+ - cohere:embed-multilingual-light-v3.0: 384
"""
embed: str
From 8eea7ac401e013a10234c6389659cc214432fd7e Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 4 Dec 2024 08:15:01 -0800
Subject: [PATCH 113/149] lib: Call ensure_config in state crud methods
- this ensures that config from context vars is merged in
---
libs/langgraph/langgraph/pregel/__init__.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py
index 62506142d..2cc462b0f 100644
--- a/libs/langgraph/langgraph/pregel/__init__.py
+++ b/libs/langgraph/langgraph/pregel/__init__.py
@@ -673,6 +673,7 @@ class Pregel(PregelProtocol):
self, config: RunnableConfig, *, subgraphs: bool = False
) -> StateSnapshot:
"""Get the current state of the graph."""
+ config = ensure_config(config)
checkpointer: Optional[BaseCheckpointSaver] = config[CONF].get(
CONFIG_KEY_CHECKPOINTER, self.checkpointer
)
@@ -710,6 +711,7 @@ class Pregel(PregelProtocol):
self, config: RunnableConfig, *, subgraphs: bool = False
) -> StateSnapshot:
"""Get the current state of the graph."""
+ config = ensure_config(config)
checkpointer: Optional[BaseCheckpointSaver] = config[CONF].get(
CONFIG_KEY_CHECKPOINTER, self.checkpointer
)
@@ -751,6 +753,7 @@ class Pregel(PregelProtocol):
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
) -> Iterator[StateSnapshot]:
+ config = ensure_config(config)
"""Get the history of the state of the graph."""
checkpointer: Optional[BaseCheckpointSaver] = config[CONF].get(
CONFIG_KEY_CHECKPOINTER, self.checkpointer
@@ -800,6 +803,7 @@ class Pregel(PregelProtocol):
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
) -> AsyncIterator[StateSnapshot]:
+ config = ensure_config(config)
"""Get the history of the state of the graph."""
checkpointer: Optional[BaseCheckpointSaver] = config[CONF].get(
CONFIG_KEY_CHECKPOINTER, self.checkpointer
@@ -855,6 +859,7 @@ class Pregel(PregelProtocol):
node `as_node`. If `as_node` is not provided, it will be set to the last node
that updated the state, if not ambiguous.
"""
+ config = ensure_config(config)
checkpointer: Optional[BaseCheckpointSaver] = config[CONF].get(
CONFIG_KEY_CHECKPOINTER, self.checkpointer
)
@@ -1130,6 +1135,7 @@ class Pregel(PregelProtocol):
values: dict[str, Any] | Any,
as_node: Optional[str] = None,
) -> RunnableConfig:
+ config = ensure_config(config)
checkpointer: Optional[BaseCheckpointSaver] = config[CONF].get(
CONFIG_KEY_CHECKPOINTER, self.checkpointer
)
From e5b00cdd1ef3f80a0aaf2ac9a2edeadcdab7763b Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 4 Dec 2024 08:30:10 -0800
Subject: [PATCH 114/149] Fix
---
libs/langgraph/langgraph/pregel/__init__.py | 16 ++++++----------
1 file changed, 6 insertions(+), 10 deletions(-)
diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py
index 2cc462b0f..e714afe21 100644
--- a/libs/langgraph/langgraph/pregel/__init__.py
+++ b/libs/langgraph/langgraph/pregel/__init__.py
@@ -673,8 +673,7 @@ class Pregel(PregelProtocol):
self, config: RunnableConfig, *, subgraphs: bool = False
) -> StateSnapshot:
"""Get the current state of the graph."""
- config = ensure_config(config)
- checkpointer: Optional[BaseCheckpointSaver] = config[CONF].get(
+ checkpointer: Optional[BaseCheckpointSaver] = ensure_config(config)[CONF].get(
CONFIG_KEY_CHECKPOINTER, self.checkpointer
)
if not checkpointer:
@@ -711,8 +710,7 @@ class Pregel(PregelProtocol):
self, config: RunnableConfig, *, subgraphs: bool = False
) -> StateSnapshot:
"""Get the current state of the graph."""
- config = ensure_config(config)
- checkpointer: Optional[BaseCheckpointSaver] = config[CONF].get(
+ checkpointer: Optional[BaseCheckpointSaver] = ensure_config(config)[CONF].get(
CONFIG_KEY_CHECKPOINTER, self.checkpointer
)
if not checkpointer:
@@ -755,7 +753,7 @@ class Pregel(PregelProtocol):
) -> Iterator[StateSnapshot]:
config = ensure_config(config)
"""Get the history of the state of the graph."""
- checkpointer: Optional[BaseCheckpointSaver] = config[CONF].get(
+ checkpointer: Optional[BaseCheckpointSaver] = ensure_config(config)[CONF].get(
CONFIG_KEY_CHECKPOINTER, self.checkpointer
)
if not checkpointer:
@@ -805,7 +803,7 @@ class Pregel(PregelProtocol):
) -> AsyncIterator[StateSnapshot]:
config = ensure_config(config)
"""Get the history of the state of the graph."""
- checkpointer: Optional[BaseCheckpointSaver] = config[CONF].get(
+ checkpointer: Optional[BaseCheckpointSaver] = ensure_config(config)[CONF].get(
CONFIG_KEY_CHECKPOINTER, self.checkpointer
)
if not checkpointer:
@@ -859,8 +857,7 @@ class Pregel(PregelProtocol):
node `as_node`. If `as_node` is not provided, it will be set to the last node
that updated the state, if not ambiguous.
"""
- config = ensure_config(config)
- checkpointer: Optional[BaseCheckpointSaver] = config[CONF].get(
+ checkpointer: Optional[BaseCheckpointSaver] = ensure_config(config)[CONF].get(
CONFIG_KEY_CHECKPOINTER, self.checkpointer
)
if not checkpointer:
@@ -1135,8 +1132,7 @@ class Pregel(PregelProtocol):
values: dict[str, Any] | Any,
as_node: Optional[str] = None,
) -> RunnableConfig:
- config = ensure_config(config)
- checkpointer: Optional[BaseCheckpointSaver] = config[CONF].get(
+ checkpointer: Optional[BaseCheckpointSaver] = ensure_config(config)[CONF].get(
CONFIG_KEY_CHECKPOINTER, self.checkpointer
)
if not checkpointer:
From 830557d6b7bc3921ade1dcd61fdd2f2e0b09c62a Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 4 Dec 2024 08:38:09 -0800
Subject: [PATCH 115/149] Clarify behavior in docstring (#2628)
---
docs/mkdocs.yml | 1 +
.../langgraph/store/postgres/aio.py | 2 ++
.../langgraph/store/postgres/base.py | 1 +
.../langgraph/store/base/__init__.py | 27 +++++++++++++------
4 files changed, 23 insertions(+), 8 deletions(-)
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 3167d5520..29d58b077 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -225,6 +225,7 @@ nav:
- cloud/deployment/setup.md
- cloud/deployment/setup_pyproject.md
- cloud/deployment/setup_javascript.md
+ - cloud/deployment/semantic_search.md
- cloud/deployment/custom_docker.md
- cloud/deployment/test_locally.md
- cloud/deployment/graph_rebuild.md
diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/aio.py b/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
index 9be44ded6..cbdd4cfc1 100644
--- a/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
+++ b/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
@@ -72,6 +72,8 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
# Store documents
await store.aput(("docs",), "doc1", {"text": "Python tutorial"})
await store.aput(("docs",), "doc2", {"text": "TypeScript guide"})
+ # Don't index the following
+ await store.aput(("docs",), "doc3", {"text": "Other guide"}, index=False)
# Search by similarity
results = await store.asearch(("docs",), query="python programming")
diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/base.py b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
index e12a59667..d0fd58da5 100644
--- a/libs/checkpoint-postgres/langgraph/store/postgres/base.py
+++ b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
@@ -569,6 +569,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
# Store documents
store.put(("docs",), "doc1", {"text": "Python tutorial"})
store.put(("docs",), "doc2", {"text": "TypeScript guide"})
+ store.put(("docs",), "doc2", {"text": "Other guide"}, index=False) # don't index
# Search by similarity
results = store.search(("docs",), query="python programming")
diff --git a/libs/checkpoint/langgraph/store/base/__init__.py b/libs/checkpoint/langgraph/store/base/__init__.py
index 3f36b7f16..6406527d3 100644
--- a/libs/checkpoint/langgraph/store/base/__init__.py
+++ b/libs/checkpoint/langgraph/store/base/__init__.py
@@ -557,10 +557,15 @@ class IndexConfig(TypedDict, total=False):
"""Fields to extract text from for embedding generation.
Controls which parts of stored items are embedded for semantic search. Follows JSON path syntax:
- - ["$"] (default): Embeds the entire JSON object as one vector
- - ["field1", "field2"]: Embeds specific top-level fields
- - ["parent.child"]: Embeds nested fields using dot notation
- - ["array[*].field"]: Embeds field from each array element separately
+
+ - ["$"]: Embeds the entire JSON object as one vector (default)
+ - ["field1", "field2"]: Embeds specific top-level fields
+ - ["parent.child"]: Embeds nested fields using dot notation
+ - ["array[*].field"]: Embeds field from each array element separately
+
+ Note:
+ You can always override this behavior when storing an item using the
+ `index` parameter in the `put` or `aput` operations.
???+ example "Examples"
```python
@@ -706,6 +711,8 @@ class BaseStore(ABC):
index: Controls how the item's fields are indexed for search:
- None (default): Use `fields` you configured when creating the store (if any)
+ If you do not initialize the store with indexing capabilities,
+ the `index` parameter will be ignored
- False: Disable indexing for this item
- list[str]: List of field paths to index, supporting:
- Nested fields: "metadata.title"
@@ -713,8 +720,9 @@ class BaseStore(ABC):
- Specific indices: "authors[0].name"
Note:
- Indexing capabilities depend on your store implementation.
- Some implementations may support only a subset of indexing features.
+ Indexing support depends on your store implementation.
+ If you do not initialize the store with indexing capabilities,
+ the `index` parameter will be ignored.
???+ example "Examples"
Store item. Indexing depends on how you configure the store.
@@ -890,6 +898,8 @@ class BaseStore(ABC):
index: Controls how the item's fields are indexed for search:
- None (default): Use `fields` you configured when creating the store (if any)
+ If you do not initialize the store with indexing capabilities,
+ the `index` parameter will be ignored
- False: Disable indexing for this item
- list[str]: List of field paths to index, supporting:
- Nested fields: "metadata.title"
@@ -897,8 +907,9 @@ class BaseStore(ABC):
- Specific indices: "authors[0].name"
Note:
- Indexing capabilities depend on your store implementation.
- Some implementations may support only a subset of indexing features.
+ Indexing support depends on your store implementation.
+ If you do not initialize the store with indexing capabilities,
+ the `index` parameter will be ignored.
???+ example "Examples"
Store item. Indexing depends on how you configure the store.
From c141f0fdf06e3dc0dd9eadb0542272a63e55efca Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 4 Dec 2024 08:39:53 -0800
Subject: [PATCH 116/149] Add memory how-to (#2629)
---
.../cassettes/semantic-search_11.msgpack.zlib | 1 +
.../cassettes/semantic-search_13.msgpack.zlib | 1 +
.../cassettes/semantic-search_15.msgpack.zlib | 1 +
.../cassettes/semantic-search_17.msgpack.zlib | 1 +
docs/cassettes/semantic-search_6.msgpack.zlib | 1 +
docs/cassettes/semantic-search_8.msgpack.zlib | 1 +
.../docs/how-tos/memory/semantic-search.ipynb | 424 ++++++++++++++++++
docs/mkdocs.yml | 1 +
poetry.lock | 43 +-
pyproject.toml | 2 +-
10 files changed, 452 insertions(+), 24 deletions(-)
create mode 100644 docs/cassettes/semantic-search_11.msgpack.zlib
create mode 100644 docs/cassettes/semantic-search_13.msgpack.zlib
create mode 100644 docs/cassettes/semantic-search_15.msgpack.zlib
create mode 100644 docs/cassettes/semantic-search_17.msgpack.zlib
create mode 100644 docs/cassettes/semantic-search_6.msgpack.zlib
create mode 100644 docs/cassettes/semantic-search_8.msgpack.zlib
create mode 100644 docs/docs/how-tos/memory/semantic-search.ipynb
diff --git a/docs/cassettes/semantic-search_11.msgpack.zlib b/docs/cassettes/semantic-search_11.msgpack.zlib
new file mode 100644
index 000000000..efa7dbf7d
--- /dev/null
+++ b/docs/cassettes/semantic-search_11.msgpack.zlib
@@ -0,0 +1 @@
+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
\ No newline at end of file
diff --git a/docs/cassettes/semantic-search_13.msgpack.zlib b/docs/cassettes/semantic-search_13.msgpack.zlib
new file mode 100644
index 000000000..c02706da9
--- /dev/null
+++ b/docs/cassettes/semantic-search_13.msgpack.zlib
@@ -0,0 +1 @@
+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
\ No newline at end of file
diff --git a/docs/cassettes/semantic-search_15.msgpack.zlib b/docs/cassettes/semantic-search_15.msgpack.zlib
new file mode 100644
index 000000000..c453ed674
--- /dev/null
+++ b/docs/cassettes/semantic-search_15.msgpack.zlib
@@ -0,0 +1 @@
+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
\ No newline at end of file
diff --git a/docs/cassettes/semantic-search_17.msgpack.zlib b/docs/cassettes/semantic-search_17.msgpack.zlib
new file mode 100644
index 000000000..f0c9ab212
--- /dev/null
+++ b/docs/cassettes/semantic-search_17.msgpack.zlib
@@ -0,0 +1 @@
+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
\ No newline at end of file
diff --git a/docs/cassettes/semantic-search_6.msgpack.zlib b/docs/cassettes/semantic-search_6.msgpack.zlib
new file mode 100644
index 000000000..cf7210983
--- /dev/null
+++ b/docs/cassettes/semantic-search_6.msgpack.zlib
@@ -0,0 +1 @@
+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
\ No newline at end of file
diff --git a/docs/cassettes/semantic-search_8.msgpack.zlib b/docs/cassettes/semantic-search_8.msgpack.zlib
new file mode 100644
index 000000000..786f820bc
--- /dev/null
+++ b/docs/cassettes/semantic-search_8.msgpack.zlib
@@ -0,0 +1 @@
+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
\ No newline at end of file
diff --git a/docs/docs/how-tos/memory/semantic-search.ipynb b/docs/docs/how-tos/memory/semantic-search.ipynb
new file mode 100644
index 000000000..0c2a6e17f
--- /dev/null
+++ b/docs/docs/how-tos/memory/semantic-search.ipynb
@@ -0,0 +1,424 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# How to add semantic search to your agent's memory\n",
+ "\n",
+ "This guide shows how to enable semantic search in your agent's memory store. This lets search for items in the store by semantic similarity.\n",
+ "\n",
+ "First, install this guide's prerequisites."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install -U langgraph langchain-openai langchain"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "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",
+ "metadata": {},
+ "source": [
+ "Next, create the store."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langchain.embeddings import init_embeddings\n",
+ "from langgraph.store.memory import InMemoryStore\n",
+ "\n",
+ "# Create store with semantic search enabled\n",
+ "embeddings = init_embeddings(\"openai:text-embedding-3-small\")\n",
+ "store = InMemoryStore(\n",
+ " index={\n",
+ " \"embed\": embeddings,\n",
+ " \"dims\": 1536,\n",
+ " }\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now let's store some memories:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Store some memories\n",
+ "store.put((\"user_123\", \"memories\"), \"1\", {\"text\": \"I love pizza\"})\n",
+ "store.put((\"user_123\", \"memories\"), \"2\", {\"text\": \"I prefer Italian food\"})\n",
+ "store.put((\"user_123\", \"memories\"), \"3\", {\"text\": \"I don't like spicy food\"})\n",
+ "store.put((\"user_123\", \"memories\"), \"3\", {\"text\": \"I am studying econometrics\"})\n",
+ "store.put((\"user_123\", \"memories\"), \"3\", {\"text\": \"I am a plumber\"})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Search memories using natural language:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Memory: I prefer Italian food (similarity: 0.46482669521168163)\n",
+ "Memory: I love pizza (similarity: 0.35514845174380766)\n",
+ "Memory: I am a plumber (similarity: 0.155698702336571)\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Find memories about food preferences\n",
+ "memories = store.search((\"user_123\", \"memories\"), query=\"I like food?\", limit=5)\n",
+ "\n",
+ "for memory in memories:\n",
+ " print(f'Memory: {memory.value[\"text\"]} (similarity: {memory.score})')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Using in your agent\n",
+ "\n",
+ "Add semantic search to any node by injecting the store:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 40,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import uuid\n",
+ "from typing import Optional\n",
+ "\n",
+ "from langchain.chat_models import init_chat_model\n",
+ "from langchain_core.tools import InjectedToolArg\n",
+ "from langgraph.store.base import BaseStore\n",
+ "from typing_extensions import Annotated\n",
+ "\n",
+ "from langgraph.prebuilt import create_react_agent\n",
+ "\n",
+ "\n",
+ "def add_memories(state, *, store: BaseStore):\n",
+ " # Search based on user's last message\n",
+ " items = store.search(\n",
+ " (\"user_123\", \"memories\"), query=state[\"messages\"][-1].content, limit=2\n",
+ " )\n",
+ " memories = \"\\n\".join(item.value[\"text\"] for item in items)\n",
+ " memories = f\"## Memories of user\\n{memories}\" if memories else \"\"\n",
+ " return [\n",
+ " {\"role\": \"system\", \"content\": f\"You are a helpful assistant.\\n{memories}\"}\n",
+ " ] + state[\"messages\"]\n",
+ "\n",
+ "\n",
+ "def upsert_memory(\n",
+ " content: str,\n",
+ " *,\n",
+ " memory_id: Optional[uuid.UUID] = None,\n",
+ " store: Annotated[BaseStore, InjectedToolArg],\n",
+ "):\n",
+ " \"\"\"Upsert a memory in the database.\"\"\"\n",
+ " mem_id = memory_id or uuid.uuid4()\n",
+ " store.put(\n",
+ " (\"user_123\", \"memories\"),\n",
+ " key=str(mem_id),\n",
+ " value={\"text\": content},\n",
+ " )\n",
+ " return f\"Stored memory {mem_id}\"\n",
+ "\n",
+ "\n",
+ "agent = create_react_agent(\n",
+ " init_chat_model(\"openai:gpt-4o-mini\"),\n",
+ " tools=[upsert_memory],\n",
+ " state_modifier=add_memories,\n",
+ " store=store,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 44,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "What are you in the mood for? Since you love Italian food and pizza, would you like some recommendations for a delicious pizza or a different Italian dish?"
+ ]
+ }
+ ],
+ "source": [
+ "async for message, metadata in agent.astream(\n",
+ " input={\"messages\": [{\"role\": \"user\", \"content\": \"I'm hungry\"}]},\n",
+ " stream_mode=\"messages\",\n",
+ "):\n",
+ " print(message.content, end=\"\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Advanced Usage\n",
+ "\n",
+ "#### Multi-vector indexing\n",
+ "\n",
+ "Store and search different aspects of memories separately to improve recall or omit certain fields from being indexed."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Configure store to embed both memory content and emotional context\n",
+ "store = InMemoryStore(\n",
+ " index={\"embed\": embeddings, \"dims\": 1536, \"fields\": [\"memory\", \"emotional_context\"]}\n",
+ ")\n",
+ "# Store memories with different content/emotion pairs\n",
+ "store.put(\n",
+ " (\"user_123\", \"memories\"),\n",
+ " \"mem1\",\n",
+ " {\n",
+ " \"memory\": \"Had pizza with friends at Mario's\",\n",
+ " \"emotional_context\": \"felt happy and connected\",\n",
+ " \"this_isnt_indexed\": \"I prefer ravioli though\",\n",
+ " },\n",
+ ")\n",
+ "store.put(\n",
+ " (\"user_123\", \"memories\"),\n",
+ " \"mem2\",\n",
+ " {\n",
+ " \"memory\": \"Ate alone at home\",\n",
+ " \"emotional_context\": \"felt a bit lonely\",\n",
+ " \"this_isnt_indexed\": \"I like pie\",\n",
+ " },\n",
+ ")\n",
+ "\n",
+ "# Search focusing on emotional state - matches mem2\n",
+ "results = store.search(\n",
+ " (\"user_123\", \"memories\"), query=\"times they felt isolated\", limit=1\n",
+ ")\n",
+ "print(\"Expect mem 2\")\n",
+ "for r in results:\n",
+ " print(f\"Item: {r.key}; Score ({r.score})\")\n",
+ " print(f\"Memory: {r.value['memory']}\")\n",
+ " print(f\"Emotion: {r.value['emotional_context']}\\n\")\n",
+ "\n",
+ "# Search focusing on social eating - matches mem1\n",
+ "print(\"Expect mem1\")\n",
+ "results = store.search((\"user_123\", \"memories\"), query=\"fun pizza\", limit=1)\n",
+ "for r in results:\n",
+ " print(f\"Item: {r.key}; Score ({r.score})\")\n",
+ " print(f\"Memory: {r.value['memory']}\")\n",
+ " print(f\"Emotion: {r.value['emotional_context']}\\n\")\n",
+ "\n",
+ "print(\"Expect random lower score (ravioli not indexed)\")\n",
+ "results = store.search((\"user_123\", \"memories\"), query=\"ravioli\", limit=1)\n",
+ "for r in results:\n",
+ " print(f\"Item: {r.key}; Score ({r.score})\")\n",
+ " print(f\"Memory: {r.value['memory']}\")\n",
+ " print(f\"Emotion: {r.value['emotional_context']}\\n\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Override fields at storage time\n",
+ "You can override which fields to embed when storing a specific memory using `put(..., index=[...fields])`, regardless of the store's default configuration."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 57,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Expect mem1\n",
+ "Item: mem1; Score (0.3374698138722726)\n",
+ "Memory: I love spicy food\n",
+ "Context: At a Thai restaurant\n",
+ "\n",
+ "Expect mem2\n",
+ "Item: mem2; Score (0.3679447999059255)\n",
+ "Memory: The restaurant was too loud\n",
+ "Context: Dinner at an Italian place\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "embeddings = init_embeddings(\"openai:text-embedding-3-small\")\n",
+ "store = InMemoryStore(\n",
+ " index={\n",
+ " \"embed\": embeddings,\n",
+ " \"dims\": 1536,\n",
+ " \"fields\": [\"memory\"],\n",
+ " } # Default to embed memory field\n",
+ ")\n",
+ "\n",
+ "# Store one memory with default indexing\n",
+ "store.put(\n",
+ " (\"user_123\", \"memories\"),\n",
+ " \"mem1\",\n",
+ " {\"memory\": \"I love spicy food\", \"context\": \"At a Thai restaurant\"},\n",
+ ")\n",
+ "\n",
+ "# Store another overriding which fields to embed\n",
+ "store.put(\n",
+ " (\"user_123\", \"memories\"),\n",
+ " \"mem2\",\n",
+ " {\"memory\": \"The restaurant was too loud\", \"context\": \"Dinner at an Italian place\"},\n",
+ " index=[\"context\"], # Override: only embed the context\n",
+ ")\n",
+ "\n",
+ "# Search about food - matches mem1 (using default field)\n",
+ "print(\"Expect mem1\")\n",
+ "results = store.search(\n",
+ " (\"user_123\", \"memories\"), query=\"what food do they like\", limit=1\n",
+ ")\n",
+ "for r in results:\n",
+ " print(f\"Item: {r.key}; Score ({r.score})\")\n",
+ " print(f\"Memory: {r.value['memory']}\")\n",
+ " print(f\"Context: {r.value['context']}\\n\")\n",
+ "\n",
+ "# Search about restaurant atmosphere - matches mem2 (using overridden field)\n",
+ "print(\"Expect mem2\")\n",
+ "results = store.search(\n",
+ " (\"user_123\", \"memories\"), query=\"restaurant environment\", limit=1\n",
+ ")\n",
+ "for r in results:\n",
+ " print(f\"Item: {r.key}; Score ({r.score})\")\n",
+ " print(f\"Memory: {r.value['memory']}\")\n",
+ " print(f\"Context: {r.value['context']}\\n\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Disable Indexing for Specific Memories\n",
+ "\n",
+ "Some memories shouldn't be searchable by content. You can disable indexing for these while still storing them using \n",
+ "`put(..., index=False)`. Example:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "store = InMemoryStore(index={\"embed\": embeddings, \"dims\": 1536, \"fields\": [\"memory\"]})\n",
+ "\n",
+ "# Store a normal indexed memory\n",
+ "store.put(\n",
+ " (\"user_123\", \"memories\"),\n",
+ " \"mem1\",\n",
+ " {\"memory\": \"I love chocolate ice cream\", \"type\": \"preference\"},\n",
+ ")\n",
+ "\n",
+ "# Store a system memory without indexing\n",
+ "store.put(\n",
+ " (\"user_123\", \"memories\"),\n",
+ " \"mem2\",\n",
+ " {\"memory\": \"User completed onboarding\", \"type\": \"system\"},\n",
+ " index=False, # Disable indexing entirely\n",
+ ")\n",
+ "\n",
+ "# Search about food preferences - finds mem1\n",
+ "print(\"Expect mem1\")\n",
+ "results = store.search((\"user_123\", \"memories\"), query=\"what food preferences\", limit=1)\n",
+ "for r in results:\n",
+ " print(f\"Item: {r.key}; Score ({r.score})\")\n",
+ " print(f\"Memory: {r.value['memory']}\")\n",
+ " print(f\"Type: {r.value['type']}\\n\")\n",
+ "\n",
+ "# Search about onboarding - won't find mem2 (not indexed)\n",
+ "print(\"Expect low score (mem2 not indexed)\")\n",
+ "results = store.search((\"user_123\", \"memories\"), query=\"onboarding status\", limit=1)\n",
+ "for r in results:\n",
+ " print(f\"Item: {r.key}; Score ({r.score})\")\n",
+ " print(f\"Memory: {r.value['memory']}\")\n",
+ " print(f\"Type: {r.value['type']}\\n\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 29d58b077..58489b12a 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -164,6 +164,7 @@ nav:
- how-tos/memory/manage-conversation-history.ipynb
- how-tos/memory/delete-messages.ipynb
- how-tos/memory/add-summary-conversation-history.ipynb
+ - how-tos/memory/semantic-search.ipynb
- Human-in-the-loop:
- Human-in-the-loop: how-tos#human-in-the-loop
- how-tos/human_in_the_loop/breakpoints.ipynb
diff --git a/poetry.lock b/poetry.lock
index d8f321637..e0a15a09c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
@@ -2862,30 +2862,30 @@ adal = ["adal (>=1.0.2)"]
[[package]]
name = "langchain"
-version = "0.3.1"
+version = "0.3.9"
description = "Building applications with LLMs through composability"
optional = false
python-versions = "<4.0,>=3.9"
files = [
- {file = "langchain-0.3.1-py3-none-any.whl", hash = "sha256:94e5ee7464d4366e4b158aa5704953c39701ea237b9ed4b200096d49e83bb3ae"},
- {file = "langchain-0.3.1.tar.gz", hash = "sha256:54d6e3abda2ec056875a231a418a4130ba7576e629e899067e499bfc847b7586"},
+ {file = "langchain-0.3.9-py3-none-any.whl", hash = "sha256:ade5a1fee2f94f2e976a6c387f97d62cc7f0b9f26cfe0132a41d2bda761e1045"},
+ {file = "langchain-0.3.9.tar.gz", hash = "sha256:4950c4ad627d0aa95ce6bda7de453e22059b7e7836b562a8f781fb0b05d7294c"},
]
[package.dependencies]
aiohttp = ">=3.8.3,<4.0.0"
async-timeout = {version = ">=4.0.0,<5.0.0", markers = "python_version < \"3.11\""}
-langchain-core = ">=0.3.6,<0.4.0"
+langchain-core = ">=0.3.21,<0.4.0"
langchain-text-splitters = ">=0.3.0,<0.4.0"
langsmith = ">=0.1.17,<0.2.0"
numpy = [
- {version = ">=1,<2", markers = "python_version < \"3.12\""},
- {version = ">=1.26.0,<2.0.0", markers = "python_version >= \"3.12\""},
+ {version = ">=1.22.4,<2", markers = "python_version < \"3.12\""},
+ {version = ">=1.26.2,<3", markers = "python_version >= \"3.12\""},
]
pydantic = ">=2.7.4,<3.0.0"
PyYAML = ">=5.3"
requests = ">=2,<3"
SQLAlchemy = ">=1.4,<3"
-tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0"
+tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10"
[[package]]
name = "langchain-anthropic"
@@ -2933,13 +2933,13 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0"
[[package]]
name = "langchain-core"
-version = "0.3.15"
+version = "0.3.21"
description = "Building applications with LLMs through composability"
optional = false
python-versions = "<4.0,>=3.9"
files = [
- {file = "langchain_core-0.3.15-py3-none-any.whl", hash = "sha256:3d4ca6dbb8ed396a6ee061063832a2451b0ce8c345570f7b086ffa7288e4fa29"},
- {file = "langchain_core-0.3.15.tar.gz", hash = "sha256:b1a29787a4ffb7ec2103b4e97d435287201da7809b369740dd1e32f176325aba"},
+ {file = "langchain_core-0.3.21-py3-none-any.whl", hash = "sha256:7e723dff80946a1198976c6876fea8326dc82566ef9bcb5f8d9188f738733665"},
+ {file = "langchain_core-0.3.21.tar.gz", hash = "sha256:561b52b258ffa50a9fb11d7a1940ebfd915654d1ec95b35e81dfd5ee84143411"},
]
[package.dependencies]
@@ -3035,7 +3035,7 @@ langchain-core = ">=0.3.0,<0.4.0"
[[package]]
name = "langgraph"
-version = "0.2.52"
+version = "0.2.54"
description = "Building stateful, multi-actor applications with LLMs"
optional = false
python-versions = ">=3.9.0,<4.0"
@@ -3045,7 +3045,7 @@ develop = true
[package.dependencies]
langchain-core = ">=0.2.43,<0.4.0,!=0.3.0,!=0.3.1,!=0.3.2,!=0.3.3,!=0.3.4,!=0.3.5,!=0.3.6,!=0.3.7,!=0.3.8,!=0.3.9,!=0.3.10,!=0.3.11,!=0.3.12,!=0.3.13,!=0.3.14"
langgraph-checkpoint = "^2.0.4"
-langgraph-sdk = "^0.1.32"
+langgraph-sdk = "^0.1.42"
[package.source]
type = "directory"
@@ -3053,7 +3053,7 @@ url = "libs/langgraph"
[[package]]
name = "langgraph-checkpoint"
-version = "2.0.5"
+version = "2.0.8"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -3070,7 +3070,7 @@ url = "libs/checkpoint"
[[package]]
name = "langgraph-checkpoint-postgres"
-version = "2.0.3"
+version = "2.0.7"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -3078,10 +3078,10 @@ files = []
develop = true
[package.dependencies]
-langgraph-checkpoint = "^2.0.2"
+langgraph-checkpoint = "^2.0.7"
orjson = ">=3.10.1"
-psycopg = "^3.0.0"
-psycopg-pool = "^3.0.0"
+psycopg = "^3.2.0"
+psycopg-pool = "^3.2.0"
[package.source]
type = "directory"
@@ -3106,7 +3106,7 @@ url = "libs/checkpoint-sqlite"
[[package]]
name = "langgraph-sdk"
-version = "0.1.36"
+version = "0.1.42"
description = "SDK for interacting with LangGraph API"
optional = false
python-versions = "^3.9.0,<4.0"
@@ -3115,7 +3115,6 @@ develop = true
[package.dependencies]
httpx = ">=0.25.2"
-httpx-sse = ">=0.4.0"
orjson = ">=3.10.1"
[package.source]
@@ -3586,7 +3585,6 @@ optional = false
python-versions = ">=3.6"
files = [
{file = "mkdocs-redirects-1.2.1.tar.gz", hash = "sha256:9420066d70e2a6bb357adf86e67023dcdca1857f97f07c7fe450f8f1fb42f861"},
- {file = "mkdocs_redirects-1.2.1-py3-none-any.whl", hash = "sha256:497089f9e0219e7389304cffefccdfa1cac5ff9509f2cb706f4c9b221726dffb"},
]
[package.dependencies]
@@ -6964,7 +6962,6 @@ description = "Automatically mock your HTTP interactions to simplify and speed u
optional = false
python-versions = ">=3.8"
files = [
- {file = "vcrpy-6.0.1-py2.py3-none-any.whl", hash = "sha256:621c3fb2d6bd8aa9f87532c688e4575bcbbde0c0afeb5ebdb7e14cac409edfdd"},
{file = "vcrpy-6.0.1.tar.gz", hash = "sha256:9e023fee7f892baa0bbda2f7da7c8ac51165c1c6e38ff8688683a12a4bde9278"},
]
@@ -7476,4 +7473,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
-content-hash = "776ee42630769f08e3896338f18ec81830166695d32d2208dc31dedb22d3b22d"
+content-hash = "cf18eed5e183fc4f7786d095540b6c9261e130750f2d1fcc427e08b78d522c61"
diff --git a/pyproject.toml b/pyproject.toml
index 31fe17172..c198ae5e4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -34,7 +34,7 @@ ruff = "^0.6.8"
jupyter = "^1.1.1"
[tool.poetry.group.test.dependencies]
-langchain = "^0.3.1"
+langchain = "^0.3.8"
langchain-openai = "^0.2.0"
langchain-anthropic = "^0.2.1"
langchain-nomic = "^0.1.3"
From 8db6a78ad99ded0da279c433afc9e3d76860a5f3 Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Wed, 4 Dec 2024 12:08:21 -0500
Subject: [PATCH 117/149] ci: update bug template (#2626)
---
.github/ISSUE_TEMPLATE/bug-report.yml | 49 +++++----------------------
1 file changed, 9 insertions(+), 40 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
index 83bbf116f..c7204a662 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -7,35 +7,29 @@ body:
value: >
Thank you for taking the time to file a bug report.
- Use this to report bugs in LangChain.
-
- If you're not certain that your issue is due to a bug in LangChain, please use [GitHub Discussions](https://github.com/langchain-ai/langchain/discussions)
- to ask for help with your issue.
+ Use this to report BUGS in LangChain. For usage questions, feature requests and general design questions, please use [GitHub Discussions](https://github.com/langchain-ai/langchain/discussions).
Relevant links to check before filing a bug report to see if your issue has already been reported, fixed or
if there's another way to solve your problem:
- [LangGraph documentation](https://langchain-ai.github.io/langgraph/).
+ [LangGraph Github Discussions](https://github.com/langchain-ai/langgraph/discussions),
+ [LangGraph Github Issues](https://github.com/langchain-ai/langgraph/issues),
+ [LangGraph how-to guides](https://langchain-ai.github.io/langgraph/how-tos/).
[LangChain documentation with the integrated search](https://python.langchain.com/docs/get_started/introduction),
[GitHub search](https://github.com/langchain-ai/langgraph),
- [LangChain Github Discussions](https://github.com/langchain-ai/langgraph/discussions),
- [LangChain Github Issues](https://github.com/langchain-ai/langgraph/issues),
- [LangChain ChatBot](https://chat.langchain.com/)
- type: checkboxes
id: checks
attributes:
label: Checked other resources
- description: Please confirm and check all the following options.
+ description: Before submitting this issue, please confirm that you have completed all the steps below by checking each option. These steps help ensure your issue is well-defined, relevant, and actionable.
options:
- - label: I added a very descriptive title to this issue.
+ - label: This is a bug, not a usage question. For questions, please use GitHub Discussions.
required: true
- - label: I searched the [LangGraph](https://langchain-ai.github.io/langgraph/)/LangChain documentation with the integrated search.
+ - label: I added a clear and detailed title that summarizes the issue.
required: true
- - label: I used the GitHub search to find a similar question and didn't find it.
+ - label: I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
required: true
- - label: I am sure that this is a bug in LangGraph/LangChain rather than my code.
- required: true
- - label: I am sure this is better as an issue [rather than a GitHub discussion](https://github.com/langchain-ai/langgraph/discussions/new/choose), since this is a LangGraph bug and not a design question.
+ - label: I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue.
required: true
- type: textarea
id: reproduction
@@ -45,14 +39,6 @@ body:
label: Example Code
description: |
Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case.
-
- If a maintainer can copy it, run it, and see it right away, there's a much higher chance that you'll be able to get help.
-
- **Important!**
-
- * Reduce your code to the minimum required to reproduce the issue if possible. This makes it much easier for others to help you.
- * Avoid screenshots when possible, as they are hard to read and (more importantly) don't allow others to copy-and-paste your code.
-
placeholder: |
from langgraph.graph import StateGraph
@@ -92,25 +78,8 @@ body:
attributes:
label: System Info
description: |
- Please share your system info with us.
-
- "pip freeze | grep langchain"
- platform (windows / linux / mac)
- python version
-
- OR if you're on a recent version of langchain-core you can paste the output of:
-
python -m langchain_core.sys_info
placeholder: |
- "pip freeze | grep langgraph"
- platform
- python version
-
- Alternatively, if you're on a recent version of langchain-core you can paste the output of:
-
python -m langchain_core.sys_info
-
- These will only surface LangChain packages, don't forget to include any other relevant
- packages you're using (if you're not sure what's relevant, you can paste the entire output of `pip freeze`).
validations:
required: true
From e5e659c5908e6fbccc111a9062a8791bdf205699 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 4 Dec 2024 09:11:13 -0800
Subject: [PATCH 118/149] Add langgraph.json snippet to concept doc (#2630)
---
docs/docs/concepts/persistence.md | 17 ++++++++++++++++-
.../langgraph/store/postgres/aio.py | 5 +++++
.../langgraph/store/postgres/base.py | 6 +++++-
.../checkpoint/langgraph/store/base/__init__.py | 15 ++++++++++++++-
.../langgraph/store/memory/__init__.py | 5 +++++
5 files changed, 45 insertions(+), 3 deletions(-)
diff --git a/docs/docs/concepts/persistence.md b/docs/docs/concepts/persistence.md
index 6637fbee0..e3f3c05cc 100644
--- a/docs/docs/concepts/persistence.md
+++ b/docs/docs/concepts/persistence.md
@@ -412,7 +412,22 @@ for update in graph.stream(
print(update)
```
-When we use the LangGraph API, either locally (e.g., in LangGraph Studio) or with LangGraph Cloud, the base store is available to use by default and does not need to be specified during graph compilation. For cloud deployments, semantic search is automatically configured based on your `langgraph.json` settings. See the [deployment guide](../deployment/semantic_search.md) for more details.
+When we use the LangGraph Platform, either locally (e.g., in LangGraph Studio) or with LangGraph Cloud, the base store is available to use by default and does not need to be specified during graph compilation. To enable semantic search, however, you **do** need to configure the indexing settings in your `langgraph.json` file. For example:
+
+```json
+{
+ ...
+ "store": {
+ "index": {
+ "embed": "openai:text-embeddings-3-small",
+ "dims": 1536,
+ "fields": ["$"]
+ }
+ }
+}
+```
+
+See the [deployment guide](../deployment/semantic_search.md) for more details and configuration options.
## Checkpointer libraries
diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/aio.py b/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
index cbdd4cfc1..4a516557a 100644
--- a/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
+++ b/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
@@ -99,6 +99,11 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
1. Call `setup()` before first use to create necessary tables and indexes
2. Have the pgvector extension available to use vector search
3. Use Python 3.10+ for async functionality
+
+ Note:
+ Semantic search is disabled by default. You can enable it by providing an `index` configuration
+ when creating the store. Without this configuration, all `index` arguments passed to
+ `put` or `aput`will have no effect.
"""
__slots__ = (
diff --git a/libs/checkpoint-postgres/langgraph/store/postgres/base.py b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
index d0fd58da5..839b2429e 100644
--- a/libs/checkpoint-postgres/langgraph/store/postgres/base.py
+++ b/libs/checkpoint-postgres/langgraph/store/postgres/base.py
@@ -573,7 +573,11 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
# Search by similarity
results = store.search(("docs",), query="python programming")
- ```
+
+ Note:
+ Semantic search is disabled by default. You can enable it by providing an `index` configuration
+ when creating the store. Without this configuration, all `index` arguments passed to
+ `put` or `aput`will have no effect.
Warning:
Make sure to call `setup()` before first use to create necessary tables and indexes.
diff --git a/libs/checkpoint/langgraph/store/base/__init__.py b/libs/checkpoint/langgraph/store/base/__init__.py
index 6406527d3..b2ab49527 100644
--- a/libs/checkpoint/langgraph/store/base/__init__.py
+++ b/libs/checkpoint/langgraph/store/base/__init__.py
@@ -471,7 +471,11 @@ class InvalidNamespaceError(ValueError):
class IndexConfig(TypedDict, total=False):
- """Configuration for indexing documents for semantic search in the store."""
+ """Configuration for indexing documents for semantic search in the store.
+
+ If not provided to the store, the store will not support vector search.
+ In that case, all `index` arguments to put() and `aput()` operations will be ignored.
+ """
dims: int
"""Number of dimensions in the embedding vectors.
@@ -595,6 +599,15 @@ class BaseStore(ABC):
Stores enable persistence and memory that can be shared across threads,
scoped to user IDs, assistant IDs, or other arbitrary namespaces.
+ Some implementations may support semantic search capabilities through
+ an optional `index` configuration.
+
+ Note:
+ Semantic search capabilities vary by implementation and are typically
+ disabled by default. Stores that support this feature can be configured
+ by providing an `index` configuration at creation time. Without this
+ configuration, semantic search is disabled and any `index` arguments
+ to storage operations will have no effect.
"""
__slots__ = ("__weakref__",)
diff --git a/libs/checkpoint/langgraph/store/memory/__init__.py b/libs/checkpoint/langgraph/store/memory/__init__.py
index d2786db48..ff2d53592 100644
--- a/libs/checkpoint/langgraph/store/memory/__init__.py
+++ b/libs/checkpoint/langgraph/store/memory/__init__.py
@@ -154,6 +154,11 @@ class InMemoryStore(BaseStore):
# Search by similarity
results = store.search(("docs",), query="python programming")
+ Note:
+ Semantic search is disabled by default. You can enable it by providing an `index` configuration
+ when creating the store. Without this configuration, all `index` arguments passed to
+ `put` or `aput`will have no effect.
+
Warning:
This store keeps all data in memory. Data is lost when the process exits.
For persistence, use a database-backed store like PostgresStore.
From 851e6d1d4c22fc4468cf26e21d4e769e2f2efe19 Mon Sep 17 00:00:00 2001
From: Vadym Barda
Date: Wed, 4 Dec 2024 12:51:00 -0500
Subject: [PATCH 119/149] issue template: replace langchain w/ langgraph
(#2631)
---
.github/ISSUE_TEMPLATE/bug-report.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
index c7204a662..f48e92bf6 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -7,7 +7,7 @@ body:
value: >
Thank you for taking the time to file a bug report.
- Use this to report BUGS in LangChain. For usage questions, feature requests and general design questions, please use [GitHub Discussions](https://github.com/langchain-ai/langchain/discussions).
+ Use this to report BUGS in LangGraph. For usage questions, feature requests and general design questions, please use [GitHub Discussions](https://github.com/langchain-ai/langgraph/discussions).
Relevant links to check before filing a bug report to see if your issue has already been reported, fixed or
if there's another way to solve your problem:
From 3ff1f8133319e8a91ae4a573a34b547c1c9923c9 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 4 Dec 2024 10:19:14 -0800
Subject: [PATCH 120/149] Add doc to index (#2632)
---
docs/docs/how-tos/index.md | 3 ++-
docs/docs/how-tos/memory/semantic-search.ipynb | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md
index e579902d9..5efbe1969 100644
--- a/docs/docs/how-tos/index.md
+++ b/docs/docs/how-tos/index.md
@@ -39,7 +39,8 @@ LangGraph makes it easy to manage conversation [memory](../concepts/memory.md) i
- [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)
-- [Add long-term memory (cross-thread)](cross-thread-persistence.ipynb)
+- [How to add long-term memory (cross-thread)](cross-thread-persistence.ipynb)
+- [How to use semantic search for long-term memory](memory/semantic-search.ipynb)
### Human-in-the-loop
diff --git a/docs/docs/how-tos/memory/semantic-search.ipynb b/docs/docs/how-tos/memory/semantic-search.ipynb
index 0c2a6e17f..8d61e18bc 100644
--- a/docs/docs/how-tos/memory/semantic-search.ipynb
+++ b/docs/docs/how-tos/memory/semantic-search.ipynb
@@ -43,7 +43,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "Next, create the store."
+ "Next, create the store with an [index configuration](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.IndexConfig). By default, stores are configured without semantic/vector search. You can opt in to indexing items when creating the store by providing an [IndexConfig](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.IndexConfig) to the store's constructor. If your store class does not implement this interface, or if you do not pass in an index configuration, semantic search is disabled, and all `index` arguments passed to `put` or `aput` will have no effect. Below is an exmaple."
]
},
{
From c89e84fb6a1cbf07ce81cefa349075087d236a86 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 4 Dec 2024 10:24:35 -0800
Subject: [PATCH 121/149] nit: Spelling (#2633)
---
docs/docs/how-tos/memory/semantic-search.ipynb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/docs/how-tos/memory/semantic-search.ipynb b/docs/docs/how-tos/memory/semantic-search.ipynb
index 8d61e18bc..24905e625 100644
--- a/docs/docs/how-tos/memory/semantic-search.ipynb
+++ b/docs/docs/how-tos/memory/semantic-search.ipynb
@@ -43,7 +43,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "Next, create the store with an [index configuration](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.IndexConfig). By default, stores are configured without semantic/vector search. You can opt in to indexing items when creating the store by providing an [IndexConfig](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.IndexConfig) to the store's constructor. If your store class does not implement this interface, or if you do not pass in an index configuration, semantic search is disabled, and all `index` arguments passed to `put` or `aput` will have no effect. Below is an exmaple."
+ "Next, create the store with an [index configuration](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.IndexConfig). By default, stores are configured without semantic/vector search. You can opt in to indexing items when creating the store by providing an [IndexConfig](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.IndexConfig) to the store's constructor. If your store class does not implement this interface, or if you do not pass in an index configuration, semantic search is disabled, and all `index` arguments passed to `put` or `aput` will have no effect. Below is an example."
]
},
{
From 962a969fbae564ce8392b7803573fd0b79e8914f Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 4 Dec 2024 12:09:04 -0800
Subject: [PATCH 122/149] Update link (#2634)
---
docs/docs/concepts/persistence.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/docs/concepts/persistence.md b/docs/docs/concepts/persistence.md
index e3f3c05cc..8f4aa993a 100644
--- a/docs/docs/concepts/persistence.md
+++ b/docs/docs/concepts/persistence.md
@@ -427,7 +427,7 @@ When we use the LangGraph Platform, either locally (e.g., in LangGraph Studio) o
}
```
-See the [deployment guide](../deployment/semantic_search.md) for more details and configuration options.
+See the [deployment guide](../cloud/deployment/semantic_search.md) for more details and configuration options.
## Checkpointer libraries
From 90eab07deddf3eb63613a2a152df7ddc4704dca0 Mon Sep 17 00:00:00 2001
From: vbarda
Date: Wed, 4 Dec 2024 12:56:39 -0500
Subject: [PATCH 123/149] docs: add Command/GraphCommand docs
---
docs/docs/concepts/low_level.md | 46 +++
docs/docs/how-tos/graph-command.ipynb | 363 ++++++++++++++++++++++++
docs/docs/how-tos/index.md | 1 +
docs/docs/reference/graphs.md | 1 +
docs/docs/reference/types.md | 1 +
docs/mkdocs.yml | 1 +
libs/langgraph/langgraph/graph/state.py | 13 +-
libs/langgraph/langgraph/types.py | 11 +-
8 files changed, 435 insertions(+), 2 deletions(-)
create mode 100644 docs/docs/how-tos/graph-command.ipynb
diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md
index 05ceacdea..9c562e093 100644
--- a/docs/docs/concepts/low_level.md
+++ b/docs/docs/concepts/low_level.md
@@ -322,6 +322,52 @@ def continue_to_jokes(state: OverallState):
graph.add_conditional_edges("node_a", continue_to_jokes)
```
+## `GraphCommand`
+
+Typically, LangGraph separates control flow (edges) from state updates (nodes). However, it is often beneficial to combine the two. For example, you might want to BOTH perform state updates AND decide which node to go next in the SAME node. LangGraph provides a way to combine control flow and node state updates using [`GraphCommand`][langgraph.graph.state.GraphCommand]. To do so, you can return a `GraphCommand` object from a node instead of a state update or `Send` objects.
+
+`GraphCommand` has the following properties:
+
+ - `goto`: optional, name of the node to navigate to next.
+ If not specified, the graph will halt after executing the current superstep.
+ - `graph`: optional, graph to send the command to. Supported values are:
+ - `None`: the current graph (default)
+ - `GraphCommand.PARENT`: parent graph.
+ - `update`: optional, state update to apply to the graph's state at the current superstep.
+ - `send`: optional, list of [`Send`](#send) objects to send to other nodes.
+ - `resume`: optional, value to resume execution with. Will be used when `interrupt()` is called.
+
+```python
+from langgraph.graph import GraphCommand, StateGraph, START
+from typing_extensions import TypedDict, Literal
+
+class State(TypedDict):
+ foo: str
+
+def my_node(state: State) -> GraphCommand[Literal["my_other_node"]]:
+ return GraphCommand(update={"foo": "bar"}, goto="my_other_node")
+
+def my_other_node(state: State):
+ return {"foo": state["foo"] + "baz"}
+
+builder = StateGraph(State)
+builder.add_edge(START, "my_node")
+builder.add_node("my_node", my_node)
+builder.add_node("my_other_node", my_other_node)
+
+graph = builder.compile()
+```
+
+With `GraphCommand` you can also achieve dynamic control flow behavior (identical to [conditional edges](#conditional-edges)):
+
+```python
+def my_node(state: State) -> GraphCommand[Literal["my_other_node", "__end__"]]:
+ if state["foo"] == "bar":
+ return GraphCommand(update={"foo": "baz"}, goto="my_other_node")
+ else:
+ return GraphCommand(goto="__end__")
+```
+
## Persistence
LangGraph provides built-in persistence for your agent's state using [checkpointers][langgraph.checkpoint.base.BaseCheckpointSaver]. Checkpointers save snapshots of the graph state at every superstep, allowing resumption at any time. This enables features like human-in-the-loop interactions, memory management, and fault-tolerance. You can even directly manipulate a graph's state after its execution using the
diff --git a/docs/docs/how-tos/graph-command.ipynb b/docs/docs/how-tos/graph-command.ipynb
new file mode 100644
index 000000000..8df59c9f4
--- /dev/null
+++ b/docs/docs/how-tos/graph-command.ipynb
@@ -0,0 +1,363 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "d33ecddc-6818-41a3-9d0d-b1b1cbcd286d",
+ "metadata": {},
+ "source": [
+ "# How to combine control flow and state updates with GraphCommand"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7c0a8d03-80b4-47fd-9b17-e26aa9b081f3",
+ "metadata": {},
+ "source": [
+ "Typically, LangGraph separates control flow (edges) and state updates (nodes). However, it is often beneficial to combine the two. For example, you might want to BOTH perform state updates AND decide which node to go next in the SAME node. LangGraph provides a way to combine control flow and node state updates using `GraphCommand`. This guide shows how you can do so."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d1c3f866-8c20-40c7-a201-35f6c9f4b680",
+ "metadata": {},
+ "source": [
+ "## Setup\n",
+ "\n",
+ "First, let's install the required packages"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "6999c7fe-31bb-4c19-946a-85c2edc57da7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install -U langgraph"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0f131c92-4744-431c-a89c-7c382a15b79f",
+ "metadata": {},
+ "source": [
+ "\n",
+ "
Set up LangSmith for LangGraph development
\n",
+ "
\n",
+ " Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started here. \n",
+ "
\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f22c228f-6882-4757-8e7e-1ca51328af4a",
+ "metadata": {},
+ "source": [
+ "Let's create a simple graph with 3 nodes: A, B and C. We will first execute node A, and then decide whether to go to Node B or Node C next based on the output of node A."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "71c8bc81-c1b4-46aa-835f-2c2849156594",
+ "metadata": {},
+ "source": [
+ "## Using edges"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9a81df3a-6489-44da-8a7e-615009ef9f59",
+ "metadata": {},
+ "source": [
+ "Let's first implement the graph with a traditional LangGraph primitives -- nodes and conditional edges. The conditional edge (`route_from_a`) will inspect the state last updated by node A and decide where to go next based on the value of the state key `foo`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "de32d339-3501-4982-a34f-8d3facc53579",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import random\n",
+ "from typing_extensions import TypedDict, Literal\n",
+ "\n",
+ "from langgraph.graph import GraphCommand, StateGraph, START\n",
+ "\n",
+ "\n",
+ "# Define graph state\n",
+ "class State(TypedDict):\n",
+ " foo: str\n",
+ "\n",
+ "\n",
+ "# Define the nodes\n",
+ "def node_a(state: State):\n",
+ " print(\"Called A\")\n",
+ " return {\"foo\": random.choice([\"a\", \"b\"])}\n",
+ "\n",
+ "def node_b(state: State):\n",
+ " print(\"Called B\")\n",
+ " return {\"foo\": state[\"foo\"] + \"b\"}\n",
+ "\n",
+ "def node_c(state: State):\n",
+ " print(\"Called C\")\n",
+ " return {\"foo\": state[\"foo\"] + \"c\"}\n",
+ "\n",
+ "# Define the conditional edges\n",
+ "def route_from_a(state: State) -> Literal[\"node_b\", \"node_c\"]:\n",
+ " if state[\"foo\"] == \"a\":\n",
+ " return \"node_b\"\n",
+ " else:\n",
+ " return \"node_c\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "87ef1325-d42f-4a6c-81e6-0058b9628b9e",
+ "metadata": {},
+ "source": [
+ "We can now create the StateGraph with the above nodes and conditional edges."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "b6e3044b-d817-4f7e-9e4f-1b3aff109670",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "builder = StateGraph(State)\n",
+ "builder.add_edge(START, \"node_a\")\n",
+ "builder.add_node(node_a)\n",
+ "builder.add_node(node_b)\n",
+ "builder.add_node(node_c)\n",
+ "builder.add_conditional_edges(\"node_a\", route_from_a)\n",
+ "\n",
+ "graph = builder.compile()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e60c8a11-ce6f-484c-ba2f-936c3d69b120",
+ "metadata": {},
+ "source": [
+ "If we run the graph multiple times, we'd see it take different paths (A -> B or A -> C) based on the random choice in node A."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "9175add8-0c08-48ee-8d70-249c5d209736",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Called A\n",
+ "Called C\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "{'foo': 'bc'}"
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "graph.invoke({\"foo\": \"\"})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "254eb3a1-bb47-4401-93fb-51a65b6b8e71",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/jpeg": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from IPython.display import display, Image\n",
+ "\n",
+ "display(Image(graph.get_graph().draw_mermaid_png()))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0c52be14-d250-4c64-99e2-ce0a201e4523",
+ "metadata": {},
+ "source": [
+ "Now let's reimplement the same graph using `GraphCommand`!"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6a08d957-b3d2-4538-bf4a-68ef90a51b98",
+ "metadata": {},
+ "source": [
+ "## Using GraphCommand"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "37107209-34d6-4414-a54e-cd3ee38e3651",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Define the nodes\n",
+ "\n",
+ "def node_a(state: State) -> GraphCommand[Literal[\"node_b\", \"node_c\"]]:\n",
+ " print(\"Called A\")\n",
+ " value = random.choice([\"a\", \"b\"])\n",
+ " # this is a replacement for the logic in route_from_a\n",
+ " if value == \"a\":\n",
+ " goto = \"node_b\"\n",
+ " else:\n",
+ " goto = \"node_c\"\n",
+ "\n",
+ " # note how GraphCommand allows you to BOTH update the graph state AND route to the next node\n",
+ " return GraphCommand(\n",
+ " # this is the state update, same as we returned from node A previously\n",
+ " update={\"foo\": value},\n",
+ " # this is a replacement for route_from_a conditional edge\n",
+ " goto=goto\n",
+ " )\n",
+ "\n",
+ "# Nodes B and C are unchanged\n",
+ "\n",
+ "def node_b(state: State):\n",
+ " print(\"Called B\")\n",
+ " # graph command can also be used \n",
+ " return {\"foo\": state[\"foo\"] + \"b\"}\n",
+ "\n",
+ "def node_c(state: State):\n",
+ " print(\"Called C\")\n",
+ " return {\"foo\": state[\"foo\"] + \"c\"}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "badc25eb-4876-482e-bb10-d763023cdaad",
+ "metadata": {},
+ "source": [
+ "We can now create the `StateGraph` with the above nodes. But notice that the graph no longer uses conditional edges! This is because control flow is defined inside `node_a`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "d6711650-4380-4551-a007-2805f49ab2d8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "\n",
+ "builder = StateGraph(State)\n",
+ "builder.add_edge(START, \"node_a\")\n",
+ "builder.add_node(node_a)\n",
+ "builder.add_node(node_b)\n",
+ "builder.add_node(node_c)\n",
+ "# NOTE: there are no edges between nodes A, B and C!\n",
+ "\n",
+ "graph = builder.compile()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0ab344c5-d634-4d7d-b3b4-edf4fa875311",
+ "metadata": {},
+ "source": [
+ "!!! important\n",
+ "\n",
+ " You might have noticed that we used `GraphCommand` as a return type annotation, e.g. `GraphCommand[Literal[\"node_b\", \"node_c\"]]`. This is necessary for the graph compilation and rendering, and tells LangGraph that `node_a` can navigate to `node_b` and `node_c`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "eeb810e5-8822-4c09-8d53-c55cd0f5d42e",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/jpeg": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "display(Image(graph.get_graph().draw_mermaid_png()))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "d88a5d9b-ee08-4ed4-9c65-6e868210bfac",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Called A\n",
+ "Called C\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "{'foo': 'bc'}"
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "graph.invoke({\"foo\": \"\"})"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md
index e579902d9..06694822c 100644
--- a/docs/docs/how-tos/index.md
+++ b/docs/docs/how-tos/index.md
@@ -20,6 +20,7 @@ These how-to guides show how to achieve that controllability.
- [How to create branches for parallel execution](branching.ipynb)
- [How to create map-reduce branches for parallel execution](map-reduce.ipynb)
- [How to control graph recursion limit](recursion-limit.ipynb)
+- [How to combine control flow and state updates with GraphCommand](graph-command.ipynb)
### Persistence
diff --git a/docs/docs/reference/graphs.md b/docs/docs/reference/graphs.md
index c67e2136a..fef38e159 100644
--- a/docs/docs/reference/graphs.md
+++ b/docs/docs/reference/graphs.md
@@ -11,6 +11,7 @@
members:
- StateGraph
- CompiledStateGraph
+ - GraphCommand
::: langgraph.graph.message
options:
diff --git a/docs/docs/reference/types.md b/docs/docs/reference/types.md
index 347a87d6e..98b1ef137 100644
--- a/docs/docs/reference/types.md
+++ b/docs/docs/reference/types.md
@@ -13,3 +13,4 @@
- PregelExecutableTask
- StateSnapshot
- Send
+ - Command
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 58489b12a..882e3cdfd 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -151,6 +151,7 @@ nav:
- how-tos/branching.ipynb
- how-tos/map-reduce.ipynb
- how-tos/recursion-limit.ipynb
+ - how-tos/graph-command.ipynb
- Persistence:
- Persistence: how-tos#persistence
- how-tos/persistence.ipynb
diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py
index 1a7208a2a..c9b4199ab 100644
--- a/libs/langgraph/langgraph/graph/state.py
+++ b/libs/langgraph/langgraph/graph/state.py
@@ -86,7 +86,18 @@ def _get_node_name(node: RunnableLike) -> str:
@dataclasses.dataclass(**_DC_KWARGS)
class GraphCommand(Generic[N], Command[N]):
- """One or more commands to update a StateGraph's state and go to, or send messages to nodes."""
+ """One or more commands to update a StateGraph's state and go to, or send messages to nodes.
+
+ Args:
+ goto: name of the node to navigate to next.
+ If not specified, the graph will halt after executing the current superstep.
+ graph: graph to send the command to. Supported values are:
+ - None: the current graph (default)
+ - GraphCommand.PARENT: closest parent graph
+ update: state update to apply to the graph's state at the current superstep.
+ send: list of `Send` objects to send to other nodes.
+ resume: value to resume execution with. Will be used when `interrupt()` is called.
+ """
goto: Union[str, Sequence[str]] = ()
diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py
index 7bf9148c5..e8407d0f1 100644
--- a/libs/langgraph/langgraph/types.py
+++ b/libs/langgraph/langgraph/types.py
@@ -239,7 +239,16 @@ N = TypeVar("N", bound=Hashable)
@dataclasses.dataclass(**_DC_KWARGS)
class Command(Generic[N]):
- """One or more commands to update the graph's state and send messages to nodes."""
+ """One or more commands to update the graph's state and send messages to nodes.
+
+ Args:
+ graph: graph to send the command to. Supported values are:
+ - None: the current graph (default)
+ - GraphCommand.PARENT: closest parent graph
+ update: state update to apply to the graph's state at the current superstep.
+ send: list of `Send` objects to send to other nodes.
+ resume: value to resume execution with. Will be used when `interrupt()` is called.
+ """
graph: Optional[str] = None
update: Optional[dict[str, Any]] = None
From 0fdf3c9daf0bf8b8fd66348ebb3ec212e9023ad3 Mon Sep 17 00:00:00 2001
From: vbarda
Date: Wed, 4 Dec 2024 16:26:31 -0500
Subject: [PATCH 124/149] cr
---
docs/docs/concepts/low_level.md | 31 ++--
docs/docs/how-tos/graph-command.ipynb | 196 ++++++------------------
libs/langgraph/langgraph/graph/state.py | 5 +-
3 files changed, 67 insertions(+), 165 deletions(-)
diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md
index 9c562e093..75ff6d4a3 100644
--- a/docs/docs/concepts/low_level.md
+++ b/docs/docs/concepts/low_level.md
@@ -324,22 +324,31 @@ graph.add_conditional_edges("node_a", continue_to_jokes)
## `GraphCommand`
-Typically, LangGraph separates control flow (edges) from state updates (nodes). However, it is often beneficial to combine the two. For example, you might want to BOTH perform state updates AND decide which node to go next in the SAME node. LangGraph provides a way to combine control flow and node state updates using [`GraphCommand`][langgraph.graph.state.GraphCommand]. To do so, you can return a `GraphCommand` object from a node instead of a state update or `Send` objects.
+It can be useful to combine control flow (edges) and state updates (nodes). For example, you might want to BOTH perform state updates AND decide which node to go to next in the SAME node. LangGraph provides a way to do so by returning a [`GraphCommand`][langgraph.graph.state.GraphCommand] object from node functions:
+
+```python
+def my_node(state: State) -> GraphCommand[Literal["my_other_node"]]:
+ return GraphCommand(
+ # state update
+ update={"foo": "bar"},
+ # control flow
+ goto="my_other_node"
+ )
+```
`GraphCommand` has the following properties:
- - `goto`: optional, name of the node to navigate to next.
- If not specified, the graph will halt after executing the current superstep.
- - `graph`: optional, graph to send the command to. Supported values are:
- - `None`: the current graph (default)
- - `GraphCommand.PARENT`: parent graph.
- - `update`: optional, state update to apply to the graph's state at the current superstep.
- - `send`: optional, list of [`Send`](#send) objects to send to other nodes.
- - `resume`: optional, value to resume execution with. Will be used when `interrupt()` is called.
+| Property | Description |
+| --- | --- |
+| `graph` | Graph to send the command to. Supported values:
- `None`: the current graph (default)
- `GraphCommand.PARENT`: parent graph |
+| `goto` | Name of the node to navigate to next. Can be any node that belongs to the specified `graph` (current or parent). If `goto` not specified, the graph will halt after executing the current superstep. |
+| `update` | State update to apply to the graph's state at the current superstep |
+| `send` | List of [`Send`](#send) objects to send to other nodes |
+| `resume` | Value to resume execution with. Will be used when `interrupt()` is called |
```python
from langgraph.graph import GraphCommand, StateGraph, START
-from typing_extensions import TypedDict, Literal
+from typing_extensions import Literal, TypedDict
class State(TypedDict):
foo: str
@@ -368,6 +377,8 @@ def my_node(state: State) -> GraphCommand[Literal["my_other_node", "__end__"]]:
return GraphCommand(goto="__end__")
```
+Check out this [how-to guide](../how-tos/graph-command.ipynb) for an end-to-end example of how to use `GraphCommand`.
+
## Persistence
LangGraph provides built-in persistence for your agent's state using [checkpointers][langgraph.checkpoint.base.BaseCheckpointSaver]. Checkpointers save snapshots of the graph state at every superstep, allowing resumption at any time. This enables features like human-in-the-loop interactions, memory management, and fault-tolerance. You can even directly manipulate a graph's state after its execution using the
diff --git a/docs/docs/how-tos/graph-command.ipynb b/docs/docs/how-tos/graph-command.ipynb
index 8df59c9f4..b215c9f4a 100644
--- a/docs/docs/how-tos/graph-command.ipynb
+++ b/docs/docs/how-tos/graph-command.ipynb
@@ -13,7 +13,27 @@
"id": "7c0a8d03-80b4-47fd-9b17-e26aa9b081f3",
"metadata": {},
"source": [
- "Typically, LangGraph separates control flow (edges) and state updates (nodes). However, it is often beneficial to combine the two. For example, you might want to BOTH perform state updates AND decide which node to go next in the SAME node. LangGraph provides a way to combine control flow and node state updates using `GraphCommand`. This guide shows how you can do so."
+ "!!! info \"Prerequisites\"\n",
+ " This guide assumes familiarity with the following:\n",
+ " \n",
+ " - [State](../../concepts/low_level/#state)\n",
+ " - [Nodes](../../concepts/low_level/#nodes)\n",
+ " - [Edges](../../concepts/low_level/#edges)\n",
+ " - [GraphCommand](../../concepts/low_level/#graphcommand)\n",
+ "\n",
+ "It can be useful to combine control flow (edges) and state updates (nodes). For example, you might want to BOTH perform state updates AND decide which node to go to next in the SAME node. LangGraph provides a way to do so by returning a `GraphCommand` object from node functions:\n",
+ "\n",
+ "```python\n",
+ "def my_node(state: State) -> GraphCommand[Literal[\"my_other_node\"]]:\n",
+ " return GraphCommand(\n",
+ " # state update\n",
+ " update={\"foo\": \"bar\"},\n",
+ " # control flow\n",
+ " goto=\"my_other_node\"\n",
+ " )\n",
+ "```\n",
+ "\n",
+ "This guide shows how you can do use `GraphCommand` to add dynamic control flow in your LangGraph app."
]
},
{
@@ -60,24 +80,16 @@
},
{
"cell_type": "markdown",
- "id": "71c8bc81-c1b4-46aa-835f-2c2849156594",
+ "id": "6a08d957-b3d2-4538-bf4a-68ef90a51b98",
"metadata": {},
"source": [
- "## Using edges"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "9a81df3a-6489-44da-8a7e-615009ef9f59",
- "metadata": {},
- "source": [
- "Let's first implement the graph with a traditional LangGraph primitives -- nodes and conditional edges. The conditional edge (`route_from_a`) will inspect the state last updated by node A and decide where to go next based on the value of the state key `foo`."
+ "## Control flow with GraphCommand"
]
},
{
"cell_type": "code",
"execution_count": 2,
- "id": "de32d339-3501-4982-a34f-8d3facc53579",
+ "id": "4539b81b-09e9-4660-ac55-1b1775e13892",
"metadata": {},
"outputs": [],
"source": [
@@ -91,142 +103,12 @@
"class State(TypedDict):\n",
" foo: str\n",
"\n",
- "\n",
- "# Define the nodes\n",
- "def node_a(state: State):\n",
- " print(\"Called A\")\n",
- " return {\"foo\": random.choice([\"a\", \"b\"])}\n",
- "\n",
- "def node_b(state: State):\n",
- " print(\"Called B\")\n",
- " return {\"foo\": state[\"foo\"] + \"b\"}\n",
- "\n",
- "def node_c(state: State):\n",
- " print(\"Called C\")\n",
- " return {\"foo\": state[\"foo\"] + \"c\"}\n",
- "\n",
- "# Define the conditional edges\n",
- "def route_from_a(state: State) -> Literal[\"node_b\", \"node_c\"]:\n",
- " if state[\"foo\"] == \"a\":\n",
- " return \"node_b\"\n",
- " else:\n",
- " return \"node_c\""
- ]
- },
- {
- "cell_type": "markdown",
- "id": "87ef1325-d42f-4a6c-81e6-0058b9628b9e",
- "metadata": {},
- "source": [
- "We can now create the StateGraph with the above nodes and conditional edges."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "id": "b6e3044b-d817-4f7e-9e4f-1b3aff109670",
- "metadata": {},
- "outputs": [],
- "source": [
- "builder = StateGraph(State)\n",
- "builder.add_edge(START, \"node_a\")\n",
- "builder.add_node(node_a)\n",
- "builder.add_node(node_b)\n",
- "builder.add_node(node_c)\n",
- "builder.add_conditional_edges(\"node_a\", route_from_a)\n",
- "\n",
- "graph = builder.compile()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "e60c8a11-ce6f-484c-ba2f-936c3d69b120",
- "metadata": {},
- "source": [
- "If we run the graph multiple times, we'd see it take different paths (A -> B or A -> C) based on the random choice in node A."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "id": "9175add8-0c08-48ee-8d70-249c5d209736",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Called A\n",
- "Called C\n"
- ]
- },
- {
- "data": {
- "text/plain": [
- "{'foo': 'bc'}"
- ]
- },
- "execution_count": 4,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "graph.invoke({\"foo\": \"\"})"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "id": "254eb3a1-bb47-4401-93fb-51a65b6b8e71",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/jpeg": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "from IPython.display import display, Image\n",
- "\n",
- "display(Image(graph.get_graph().draw_mermaid_png()))"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "0c52be14-d250-4c64-99e2-ce0a201e4523",
- "metadata": {},
- "source": [
- "Now let's reimplement the same graph using `GraphCommand`!"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "6a08d957-b3d2-4538-bf4a-68ef90a51b98",
- "metadata": {},
- "source": [
- "## Using GraphCommand"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "37107209-34d6-4414-a54e-cd3ee38e3651",
- "metadata": {},
- "outputs": [],
- "source": [
"# Define the nodes\n",
"\n",
"def node_a(state: State) -> GraphCommand[Literal[\"node_b\", \"node_c\"]]:\n",
" print(\"Called A\")\n",
" value = random.choice([\"a\", \"b\"])\n",
- " # this is a replacement for the logic in route_from_a\n",
+ " # this is a replacement for a conditional edge function\n",
" if value == \"a\":\n",
" goto = \"node_b\"\n",
" else:\n",
@@ -234,9 +116,9 @@
"\n",
" # note how GraphCommand allows you to BOTH update the graph state AND route to the next node\n",
" return GraphCommand(\n",
- " # this is the state update, same as we returned from node A previously\n",
+ " # this is the state update\n",
" update={\"foo\": value},\n",
- " # this is a replacement for route_from_a conditional edge\n",
+ " # this is a replacement for an edge\n",
" goto=goto\n",
" )\n",
"\n",
@@ -257,17 +139,16 @@
"id": "badc25eb-4876-482e-bb10-d763023cdaad",
"metadata": {},
"source": [
- "We can now create the `StateGraph` with the above nodes. But notice that the graph no longer uses conditional edges! This is because control flow is defined inside `node_a`."
+ "We can now create the `StateGraph` with the above nodes. Notice that the graph doesn't have [conditional edges](../../concepts/low_level#conditional-edges) for routing! This is because control flow is defined with `GraphCommand` inside `node_a`."
]
},
{
"cell_type": "code",
- "execution_count": 7,
+ "execution_count": 3,
"id": "d6711650-4380-4551-a007-2805f49ab2d8",
"metadata": {},
"outputs": [],
"source": [
- "\n",
"builder = StateGraph(State)\n",
"builder.add_edge(START, \"node_a\")\n",
"builder.add_node(node_a)\n",
@@ -290,7 +171,7 @@
},
{
"cell_type": "code",
- "execution_count": 8,
+ "execution_count": 4,
"id": "eeb810e5-8822-4c09-8d53-c55cd0f5d42e",
"metadata": {},
"outputs": [
@@ -306,12 +187,21 @@
}
],
"source": [
+ "from IPython.display import display, Image\n",
"display(Image(graph.get_graph().draw_mermaid_png()))"
]
},
+ {
+ "cell_type": "markdown",
+ "id": "58fb6c32-e6fb-4c94-8182-e351ed52a45d",
+ "metadata": {},
+ "source": [
+ "If we run the graph multiple times, we'd see it take different paths (A -> B or A -> C) based on the random choice in node A."
+ ]
+ },
{
"cell_type": "code",
- "execution_count": 9,
+ "execution_count": 5,
"id": "d88a5d9b-ee08-4ed4-9c65-6e868210bfac",
"metadata": {},
"outputs": [
@@ -320,16 +210,16 @@
"output_type": "stream",
"text": [
"Called A\n",
- "Called C\n"
+ "Called B\n"
]
},
{
"data": {
"text/plain": [
- "{'foo': 'bc'}"
+ "{'foo': 'ab'}"
]
},
- "execution_count": 9,
+ "execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py
index c9b4199ab..d3f608356 100644
--- a/libs/langgraph/langgraph/graph/state.py
+++ b/libs/langgraph/langgraph/graph/state.py
@@ -89,14 +89,15 @@ class GraphCommand(Generic[N], Command[N]):
"""One or more commands to update a StateGraph's state and go to, or send messages to nodes.
Args:
- goto: name of the node to navigate to next.
- If not specified, the graph will halt after executing the current superstep.
graph: graph to send the command to. Supported values are:
- None: the current graph (default)
- GraphCommand.PARENT: closest parent graph
update: state update to apply to the graph's state at the current superstep.
send: list of `Send` objects to send to other nodes.
resume: value to resume execution with. Will be used when `interrupt()` is called.
+ goto: name of the node to navigate to next.
+ Can be any node that belongs to the specified `graph` (current or parent).
+ If `goto` not specified, the graph will halt after executing the current superstep.
"""
goto: Union[str, Sequence[str]] = ()
From d52bb911a4d3f78519d786c96ebd8cce84433bc0 Mon Sep 17 00:00:00 2001
From: vbarda
Date: Wed, 4 Dec 2024 16:50:56 -0500
Subject: [PATCH 125/149] lint
---
docs/docs/how-tos/graph-command.ipynb | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/docs/docs/how-tos/graph-command.ipynb b/docs/docs/how-tos/graph-command.ipynb
index b215c9f4a..b768e75a8 100644
--- a/docs/docs/how-tos/graph-command.ipynb
+++ b/docs/docs/how-tos/graph-command.ipynb
@@ -103,8 +103,10 @@
"class State(TypedDict):\n",
" foo: str\n",
"\n",
+ "\n",
"# Define the nodes\n",
"\n",
+ "\n",
"def node_a(state: State) -> GraphCommand[Literal[\"node_b\", \"node_c\"]]:\n",
" print(\"Called A\")\n",
" value = random.choice([\"a\", \"b\"])\n",
@@ -119,16 +121,19 @@
" # this is the state update\n",
" update={\"foo\": value},\n",
" # this is a replacement for an edge\n",
- " goto=goto\n",
+ " goto=goto,\n",
" )\n",
"\n",
+ "\n",
"# Nodes B and C are unchanged\n",
"\n",
+ "\n",
"def node_b(state: State):\n",
" print(\"Called B\")\n",
- " # graph command can also be used \n",
+ " # graph command can also be used\n",
" return {\"foo\": state[\"foo\"] + \"b\"}\n",
"\n",
+ "\n",
"def node_c(state: State):\n",
" print(\"Called C\")\n",
" return {\"foo\": state[\"foo\"] + \"c\"}"
@@ -188,6 +193,7 @@
],
"source": [
"from IPython.display import display, Image\n",
+ "\n",
"display(Image(graph.get_graph().draw_mermaid_png()))"
]
},
From ea5ccd7a80e11e53002486859ca6e0004d289518 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 4 Dec 2024 14:15:30 -0800
Subject: [PATCH 126/149] lib: Add support for multiple interrupts per node
- Includes support for interrupt loops
---
libs/langgraph/langgraph/constants.py | 4 +
libs/langgraph/langgraph/pregel/algo.py | 35 +++--
libs/langgraph/langgraph/pregel/io.py | 9 +-
libs/langgraph/langgraph/pregel/loop.py | 25 +++-
libs/langgraph/langgraph/pregel/runner.py | 3 +
libs/langgraph/langgraph/types.py | 66 +++++++--
libs/langgraph/tests/test_pregel.py | 140 +++++++++++++++++++
libs/langgraph/tests/test_pregel_async.py | 157 ++++++++++++++++++++++
8 files changed, 403 insertions(+), 36 deletions(-)
diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py
index 478f23d68..efdf65143 100644
--- a/libs/langgraph/langgraph/constants.py
+++ b/libs/langgraph/langgraph/constants.py
@@ -75,6 +75,10 @@ CONFIG_KEY_NODE_FINISHED = sys.intern("__pregel_node_finished")
# callback to be called when a node is finished
CONFIG_KEY_RESUME_VALUE = sys.intern("__pregel_resume_value")
# holds the value that "answers" an interrupt() call
+CONFIG_KEY_WRITES = sys.intern("__pregel_writes")
+# read-only list of existing task writes
+CONFIG_KEY_SCRATCHPAD = sys.intern("__pregel_scratchpad")
+# holds a mutable dict for temporary storage scoped to the current task
# --- Other constants ---
PUSH = sys.intern("__pregel_push")
diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py
index 5d104a85f..0885f12aa 100644
--- a/libs/langgraph/langgraph/pregel/algo.py
+++ b/libs/langgraph/langgraph/pregel/algo.py
@@ -37,13 +37,13 @@ from langgraph.constants import (
CONFIG_KEY_CHECKPOINT_NS,
CONFIG_KEY_CHECKPOINTER,
CONFIG_KEY_READ,
- CONFIG_KEY_RESUME_VALUE,
+ CONFIG_KEY_SCRATCHPAD,
CONFIG_KEY_SEND,
CONFIG_KEY_STORE,
CONFIG_KEY_TASK_ID,
+ CONFIG_KEY_WRITES,
EMPTY_SEQ,
INTERRUPT,
- MISSING,
NO_WRITES,
NS_END,
NS_SEP,
@@ -589,14 +589,13 @@ def prepare_single_task(
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
- CONFIG_KEY_RESUME_VALUE: next(
- (
- v
- for tid, c, v in pending_writes
- if tid in (NULL_TASK_ID, task_id) and c == RESUME
- ),
- configurable.get(CONFIG_KEY_RESUME_VALUE, MISSING),
- ),
+ CONFIG_KEY_WRITES: [
+ w
+ for w in pending_writes
+ + configurable.get(CONFIG_KEY_WRITES, [])
+ if w[0] in (NULL_TASK_ID, task_id)
+ ],
+ CONFIG_KEY_SCRATCHPAD: {},
},
),
triggers,
@@ -713,15 +712,13 @@ def prepare_single_task(
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
- CONFIG_KEY_RESUME_VALUE: next(
- (
- v
- for tid, c, v in pending_writes
- if tid in (NULL_TASK_ID, task_id)
- and c == RESUME
- ),
- configurable.get(CONFIG_KEY_RESUME_VALUE, MISSING),
- ),
+ CONFIG_KEY_WRITES: [
+ w
+ for w in pending_writes
+ + configurable.get(CONFIG_KEY_WRITES, [])
+ if w[0] in (NULL_TASK_ID, task_id)
+ ],
+ CONFIG_KEY_SCRATCHPAD: {},
},
),
triggers,
diff --git a/libs/langgraph/langgraph/pregel/io.py b/libs/langgraph/langgraph/pregel/io.py
index 693dffce2..918f3d899 100644
--- a/libs/langgraph/langgraph/pregel/io.py
+++ b/libs/langgraph/langgraph/pregel/io.py
@@ -4,6 +4,7 @@ from uuid import UUID
from langchain_core.runnables.utils import AddableDict
from langgraph.channels.base import BaseChannel, EmptyChannelError
+from langgraph.checkpoint.base import PendingWrite
from langgraph.constants import (
EMPTY_SEQ,
ERROR,
@@ -66,7 +67,7 @@ def read_channels(
def map_command(
- cmd: Command,
+ cmd: Command, pending_writes: list[PendingWrite]
) -> Iterator[tuple[str, str, Any]]:
"""Map input chunk to a sequence of pending writes in the form (channel, value)."""
if cmd.graph == Command.PARENT:
@@ -85,7 +86,11 @@ def map_command(
if cmd.resume:
if isinstance(cmd.resume, dict) and all(is_task_id(k) for k in cmd.resume):
for tid, resume in cmd.resume.items():
- yield (tid, RESUME, resume)
+ existing = next(
+ (w for w in pending_writes if w[0] == tid and w[1] == RESUME), []
+ )
+ existing.append(resume)
+ yield (tid, RESUME, existing)
else:
yield (NULL_TASK_ID, RESUME, cmd.resume)
if cmd.update:
diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py
index 2a68b00f2..d9af9279e 100644
--- a/libs/langgraph/langgraph/pregel/loop.py
+++ b/libs/langgraph/langgraph/pregel/loop.py
@@ -26,6 +26,7 @@ from typing_extensions import ParamSpec, Self
from langgraph.channels.base import BaseChannel
from langgraph.checkpoint.base import (
+ WRITES_IDX_MAP,
BaseCheckpointSaver,
ChannelVersions,
Checkpoint,
@@ -263,8 +264,28 @@ class PregelLoop(LoopProtocol):
"""Put writes for a task, to be read by the next tick."""
if not writes:
return
+ # deduplicate writes to special channels, last write wins
+ if all(w[0] in WRITES_IDX_MAP for w in writes):
+ writes = list({w[0]: w for w in writes}.values())
# save writes
- self.checkpoint_pending_writes.extend((task_id, k, v) for k, v in writes)
+ for c, v in writes:
+ if (
+ c in WRITES_IDX_MAP
+ and (
+ idx := next(
+ (
+ i
+ for i, w in enumerate(self.checkpoint_pending_writes)
+ if w[0] == task_id and w[1] == c
+ ),
+ None,
+ )
+ )
+ is not None
+ ):
+ self.checkpoint_pending_writes[idx] = (task_id, c, v)
+ else:
+ self.checkpoint_pending_writes.append((task_id, c, v))
if self.checkpointer_put_writes is not None:
self.submit(
self.checkpointer_put_writes,
@@ -536,7 +557,7 @@ class PregelLoop(LoopProtocol):
elif isinstance(self.input, Command):
writes: defaultdict[str, list[tuple[str, Any]]] = defaultdict(list)
# group writes by task ID
- for tid, c, v in map_command(self.input):
+ for tid, c, v in map_command(self.input, self.checkpoint_pending_writes):
writes[tid].append((c, v))
if not writes:
raise EmptyInputError("Received empty Command input")
diff --git a/libs/langgraph/langgraph/pregel/runner.py b/libs/langgraph/langgraph/pregel/runner.py
index 9e3879b0f..f46210459 100644
--- a/libs/langgraph/langgraph/pregel/runner.py
+++ b/libs/langgraph/langgraph/pregel/runner.py
@@ -21,6 +21,7 @@ from langgraph.constants import (
INTERRUPT,
NO_WRITES,
PUSH,
+ RESUME,
TAG_HIDDEN,
)
from langgraph.errors import GraphBubbleUp, GraphInterrupt
@@ -297,6 +298,8 @@ class PregelRunner:
if isinstance(exception, GraphInterrupt):
# save interrupt to checkpointer
if interrupts := [(INTERRUPT, i) for i in exception.args[0]]:
+ if resumes := [w for w in task.writes if w[0] == RESUME]:
+ interrupts.extend(resumes)
self.put_writes(task.id, interrupts)
elif isinstance(exception, GraphBubbleUp):
raise exception
diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py
index 7bf9148c5..4047a28f7 100644
--- a/libs/langgraph/langgraph/types.py
+++ b/libs/langgraph/langgraph/types.py
@@ -13,6 +13,7 @@ from typing import (
Optional,
Sequence,
Type,
+ TypedDict,
TypeVar,
Union,
cast,
@@ -21,11 +22,16 @@ from typing import (
from langchain_core.runnables import Runnable, RunnableConfig
from typing_extensions import Self
-from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointMetadata
+from langgraph.checkpoint.base import (
+ BaseCheckpointSaver,
+ CheckpointMetadata,
+ PendingWrite,
+)
if TYPE_CHECKING:
from langgraph.store.base import BaseStore
+
All = Literal["*"]
"""Special value to indicate that graph should interrupt on all nodes."""
@@ -300,26 +306,60 @@ class LoopProtocol:
self.stop = stop
+class PregelScratchpad(TypedDict, total=False):
+ interrupt_counter: int
+ used_null_resume: bool
+ resume: list[Any]
+
+
def interrupt(value: Any) -> Any:
from langgraph.constants import (
CONFIG_KEY_CHECKPOINT_NS,
- CONFIG_KEY_RESUME_VALUE,
- MISSING,
+ CONFIG_KEY_SCRATCHPAD,
+ CONFIG_KEY_SEND,
+ CONFIG_KEY_TASK_ID,
+ CONFIG_KEY_WRITES,
NS_SEP,
+ NULL_TASK_ID,
+ RESUME,
)
from langgraph.errors import GraphInterrupt
from langgraph.utils.config import get_configurable
conf = get_configurable()
- if (resume := conf.get(CONFIG_KEY_RESUME_VALUE, MISSING)) and resume is not MISSING:
- return resume
+ # track interrupt index
+ scratchpad: PregelScratchpad = conf[CONFIG_KEY_SCRATCHPAD]
+ if "interrupt_counter" not in scratchpad:
+ scratchpad["interrupt_counter"] = 0
else:
- raise GraphInterrupt(
- (
- Interrupt(
- value=value,
- resumable=True,
- ns=cast(str, conf[CONFIG_KEY_CHECKPOINT_NS]).split(NS_SEP),
- ),
- )
+ scratchpad["interrupt_counter"] += 1
+ idx = scratchpad["interrupt_counter"]
+ # find previous resume values
+ task_id = conf[CONFIG_KEY_TASK_ID]
+ writes: list[PendingWrite] = conf[CONFIG_KEY_WRITES]
+ scratchpad.setdefault(
+ "resume", next((w[2] for w in writes if w[0] == task_id and w[1] == RESUME), [])
+ )
+ if scratchpad["resume"]:
+ if idx < len(scratchpad["resume"]):
+ return scratchpad["resume"][idx]
+ # find current resume value
+ if not scratchpad.get("used_null_resume"):
+ scratchpad["used_null_resume"] = True
+ for tid, c, v in sorted(writes, key=lambda x: x[0], reverse=True):
+ if tid == NULL_TASK_ID and c == RESUME:
+ assert len(scratchpad["resume"]) == idx, (scratchpad["resume"], idx)
+ scratchpad["resume"].append(v)
+ print("saving:", scratchpad["resume"])
+ conf[CONFIG_KEY_SEND]([(RESUME, scratchpad["resume"])])
+ return v
+ # no resume value found
+ raise GraphInterrupt(
+ (
+ Interrupt(
+ value=value,
+ resumable=True,
+ ns=cast(str, conf[CONFIG_KEY_CHECKPOINT_NS]).split(NS_SEP),
+ ),
)
+ )
diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py
index 4ff56ca3e..4f768badd 100644
--- a/libs/langgraph/tests/test_pregel.py
+++ b/libs/langgraph/tests/test_pregel.py
@@ -14680,3 +14680,143 @@ def test_interrupt_subgraph(request: pytest.FixtureRequest, checkpointer_name: s
assert graph.invoke({"baz": ""}, thread1)
# Resume with answer
assert graph.invoke(Command(resume="bar"), thread1)
+
+
+@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
+def test_interrupt_multiple(request: pytest.FixtureRequest, checkpointer_name: str):
+ checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
+
+ class State(TypedDict):
+ my_key: Annotated[str, operator.add]
+
+ def node(s: State) -> State:
+ answer = interrupt({"value": 1})
+ answer2 = interrupt({"value": 2})
+ return {"my_key": answer + " " + answer2}
+
+ builder = StateGraph(State)
+ builder.add_node("node", node)
+ builder.add_edge(START, "node")
+
+ graph = builder.compile(checkpointer=checkpointer)
+ thread1 = {"configurable": {"thread_id": "1"}}
+
+ assert [e for e in graph.stream({"my_key": "DE", "market": "DE"}, thread1)] == [
+ {
+ "__interrupt__": (
+ Interrupt(
+ value={"value": 1},
+ resumable=True,
+ ns=[AnyStr("node:")],
+ when="during",
+ ),
+ )
+ }
+ ]
+
+ assert [
+ event
+ for event in graph.stream(
+ Command(resume="answer 1", update={"my_key": "foofoo"}), thread1
+ )
+ ] == [
+ {
+ "__interrupt__": (
+ Interrupt(
+ value={"value": 2},
+ resumable=True,
+ ns=[AnyStr("node:")],
+ when="during",
+ ),
+ )
+ }
+ ]
+
+ assert [event for event in graph.stream(Command(resume="answer 2"), thread1)] == [
+ {"node": {"my_key": "answer 1 answer 2"}},
+ ]
+
+
+@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
+def test_interrupt_loop(request: pytest.FixtureRequest, checkpointer_name: str):
+ checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
+
+ class State(TypedDict):
+ age: int
+ other: str
+
+ def ask_age(s: State):
+ """Ask an expert for help."""
+ question = "How old are you?"
+ value = None
+ for _ in range(10):
+ value: str = interrupt(question)
+ if not value.isdigit() or int(value) < 18:
+ question = "invalid response"
+ value = None
+ else:
+ break
+
+ return {"age": int(value)}
+
+ builder = StateGraph(State)
+ builder.add_node("node", ask_age)
+ builder.add_edge(START, "node")
+
+ graph = builder.compile(checkpointer=checkpointer)
+ thread1 = {"configurable": {"thread_id": "1"}}
+
+ assert [e for e in graph.stream({"other": ""}, thread1)] == [
+ {
+ "__interrupt__": (
+ Interrupt(
+ value="How old are you?",
+ resumable=True,
+ ns=[AnyStr("node:")],
+ when="during",
+ ),
+ )
+ }
+ ]
+
+ assert [
+ event
+ for event in graph.stream(
+ Command(resume="13"),
+ thread1,
+ )
+ ] == [
+ {
+ "__interrupt__": (
+ Interrupt(
+ value="invalid response",
+ resumable=True,
+ ns=[AnyStr("node:")],
+ when="during",
+ ),
+ )
+ }
+ ]
+
+ assert [
+ event
+ for event in graph.stream(
+ Command(resume="15"),
+ thread1,
+ )
+ ] == [
+ {
+ "__interrupt__": (
+ Interrupt(
+ value="invalid response",
+ resumable=True,
+ ns=[AnyStr("node:")],
+ when="during",
+ ),
+ )
+ }
+ ]
+
+ assert [event for event in graph.stream(Command(resume="19"), thread1)] == [
+ {"node": {"age": 19}},
+ ]
diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py
index cc244c363..addb18b2b 100644
--- a/libs/langgraph/tests/test_pregel_async.py
+++ b/libs/langgraph/tests/test_pregel_async.py
@@ -12896,3 +12896,160 @@ async def test_interrupt_subgraph(checkpointer_name: str):
assert await graph.ainvoke({"baz": ""}, thread1)
# Resume with answer
assert await graph.ainvoke(Command(resume="bar"), thread1)
+
+
+@pytest.mark.skipif(
+ sys.version_info < (3, 11),
+ reason="Python 3.11+ is required for async contextvars support",
+)
+@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
+async def test_interrupt_multiple(checkpointer_name: str):
+ class State(TypedDict):
+ my_key: Annotated[str, operator.add]
+
+ async def node(s: State) -> State:
+ answer = interrupt({"value": 1})
+ answer2 = interrupt({"value": 2})
+ return {"my_key": answer + " " + answer2}
+
+ builder = StateGraph(State)
+ builder.add_node("node", node)
+ builder.add_edge(START, "node")
+
+ async with awith_checkpointer(checkpointer_name) as checkpointer:
+ graph = builder.compile(checkpointer=checkpointer)
+ thread1 = {"configurable": {"thread_id": "1"}}
+
+ assert [
+ e async for e in graph.astream({"my_key": "DE", "market": "DE"}, thread1)
+ ] == [
+ {
+ "__interrupt__": (
+ Interrupt(
+ value={"value": 1},
+ resumable=True,
+ ns=[AnyStr("node:")],
+ when="during",
+ ),
+ )
+ }
+ ]
+
+ assert [
+ event
+ async for event in graph.astream(
+ Command(resume="answer 1", update={"my_key": "foofoo"}),
+ thread1,
+ stream_mode="updates",
+ )
+ ] == [
+ {
+ "__interrupt__": (
+ Interrupt(
+ value={"value": 2},
+ resumable=True,
+ ns=[AnyStr("node:")],
+ when="during",
+ ),
+ )
+ }
+ ]
+
+ assert [
+ event
+ async for event in graph.astream(
+ Command(resume="answer 2"), thread1, stream_mode="updates"
+ )
+ ] == [
+ {"node": {"my_key": "answer 1 answer 2"}},
+ ]
+
+
+@pytest.mark.skipif(
+ sys.version_info < (3, 11),
+ reason="Python 3.11+ is required for async contextvars support",
+)
+@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
+async def test_interrupt_loop(checkpointer_name: str):
+ class State(TypedDict):
+ age: int
+ other: str
+
+ async def ask_age(s: State):
+ """Ask an expert for help."""
+ question = "How old are you?"
+ value = None
+ for _ in range(10):
+ value: str = interrupt(question)
+ if not value.isdigit() or int(value) < 18:
+ question = "invalid response"
+ value = None
+ else:
+ break
+
+ return {"age": int(value)}
+
+ builder = StateGraph(State)
+ builder.add_node("node", ask_age)
+ builder.add_edge(START, "node")
+
+ async with awith_checkpointer(checkpointer_name) as checkpointer:
+ graph = builder.compile(checkpointer=checkpointer)
+ thread1 = {"configurable": {"thread_id": "1"}}
+
+ assert [e async for e in graph.astream({"other": ""}, thread1)] == [
+ {
+ "__interrupt__": (
+ Interrupt(
+ value="How old are you?",
+ resumable=True,
+ ns=[AnyStr("node:")],
+ when="during",
+ ),
+ )
+ }
+ ]
+
+ assert [
+ event
+ async for event in graph.astream(
+ Command(resume="13"),
+ thread1,
+ )
+ ] == [
+ {
+ "__interrupt__": (
+ Interrupt(
+ value="invalid response",
+ resumable=True,
+ ns=[AnyStr("node:")],
+ when="during",
+ ),
+ )
+ }
+ ]
+
+ assert [
+ event
+ async for event in graph.astream(
+ Command(resume="15"),
+ thread1,
+ )
+ ] == [
+ {
+ "__interrupt__": (
+ Interrupt(
+ value="invalid response",
+ resumable=True,
+ ns=[AnyStr("node:")],
+ when="during",
+ ),
+ )
+ }
+ ]
+
+ assert [
+ event async for event in graph.astream(Command(resume="19"), thread1)
+ ] == [
+ {"node": {"age": 19}},
+ ]
From fb01d65dc07af9a46ff05a68dabb5ec57f1fb9d6 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 4 Dec 2024 14:31:22 -0800
Subject: [PATCH 127/149] Lint
---
libs/langgraph/langgraph/pregel/io.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/libs/langgraph/langgraph/pregel/io.py b/libs/langgraph/langgraph/pregel/io.py
index 918f3d899..ed2c28938 100644
--- a/libs/langgraph/langgraph/pregel/io.py
+++ b/libs/langgraph/langgraph/pregel/io.py
@@ -86,8 +86,8 @@ def map_command(
if cmd.resume:
if isinstance(cmd.resume, dict) and all(is_task_id(k) for k in cmd.resume):
for tid, resume in cmd.resume.items():
- existing = next(
- (w for w in pending_writes if w[0] == tid and w[1] == RESUME), []
+ existing: list[Any] = next(
+ (w[2] for w in pending_writes if w[0] == tid and w[1] == RESUME), []
)
existing.append(resume)
yield (tid, RESUME, existing)
From 5c7a6689af406faeaabf251a2f6009256eb9521a Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 4 Dec 2024 14:41:30 -0800
Subject: [PATCH 128/149] Update tests
---
libs/langgraph/langgraph/constants.py | 2 --
libs/scheduler-kafka/tests/any.py | 18 +++++++++++++++++
libs/scheduler-kafka/tests/test_subgraph.py | 20 ++++++++++++-------
.../tests/test_subgraph_sync.py | 20 ++++++++++++-------
4 files changed, 44 insertions(+), 16 deletions(-)
diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py
index efdf65143..e2d9f069a 100644
--- a/libs/langgraph/langgraph/constants.py
+++ b/libs/langgraph/langgraph/constants.py
@@ -72,8 +72,6 @@ CONFIG_KEY_CHECKPOINT_ID = sys.intern("checkpoint_id")
CONFIG_KEY_CHECKPOINT_NS = sys.intern("checkpoint_ns")
# holds the current checkpoint_ns, "" for root graph
CONFIG_KEY_NODE_FINISHED = sys.intern("__pregel_node_finished")
-# callback to be called when a node is finished
-CONFIG_KEY_RESUME_VALUE = sys.intern("__pregel_resume_value")
# holds the value that "answers" an interrupt() call
CONFIG_KEY_WRITES = sys.intern("__pregel_writes")
# read-only list of existing task writes
diff --git a/libs/scheduler-kafka/tests/any.py b/libs/scheduler-kafka/tests/any.py
index 73744a1e8..3ea224173 100644
--- a/libs/scheduler-kafka/tests/any.py
+++ b/libs/scheduler-kafka/tests/any.py
@@ -35,3 +35,21 @@ class AnyDict(dict):
return False
else:
return True
+
+
+class AnyList(list):
+ def __init__(self, *args, **kwargs) -> None:
+ super().__init__(*args, **kwargs)
+
+ def __eq__(self, other: object) -> bool:
+ if not self and isinstance(other, list):
+ return True
+ if not isinstance(other, list) or len(self) != len(other):
+ return False
+ for i, v in enumerate(self):
+ if v == other[i]:
+ continue
+ else:
+ return False
+ else:
+ return True
diff --git a/libs/scheduler-kafka/tests/test_subgraph.py b/libs/scheduler-kafka/tests/test_subgraph.py
index ebaaea580..4ab92676c 100644
--- a/libs/scheduler-kafka/tests/test_subgraph.py
+++ b/libs/scheduler-kafka/tests/test_subgraph.py
@@ -15,7 +15,7 @@ from langgraph.graph.state import StateGraph
from langgraph.pregel import Pregel
from langgraph.scheduler.kafka import serde
from langgraph.scheduler.kafka.types import MessageToOrchestrator, Topics
-from tests.any import AnyDict
+from tests.any import AnyDict, AnyList
from tests.drain import drain_topics_async
from tests.messages import _AnyIdAIMessage, _AnyIdHumanMessage
@@ -196,7 +196,8 @@ async def test_subgraph_w_interrupt(
"__pregel_resuming": False,
"__pregel_store": None,
"__pregel_task_id": history[0].tasks[0].id,
- "__pregel_resume_value": None,
+ "__pregel_scratchpad": {},
+ "__pregel_writes": AnyList(),
"checkpoint_id": None,
"checkpoint_map": {
"": history[0].config["configurable"]["checkpoint_id"]
@@ -261,7 +262,8 @@ async def test_subgraph_w_interrupt(
"__pregel_resuming": False,
"__pregel_store": None,
"__pregel_task_id": history[0].tasks[0].id,
- "__pregel_resume_value": None,
+ "__pregel_scratchpad": {},
+ "__pregel_writes": AnyList(),
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[0].config["configurable"]["checkpoint_id"]
@@ -356,7 +358,8 @@ async def test_subgraph_w_interrupt(
"__pregel_resuming": False,
"__pregel_store": None,
"__pregel_task_id": history[0].tasks[0].id,
- "__pregel_resume_value": None,
+ "__pregel_scratchpad": {},
+ "__pregel_writes": AnyList(),
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[0].config["configurable"]["checkpoint_id"]
@@ -461,7 +464,8 @@ async def test_subgraph_w_interrupt(
"__pregel_resuming": True,
"__pregel_store": None,
"__pregel_task_id": history[1].tasks[0].id,
- "__pregel_resume_value": None,
+ "__pregel_scratchpad": {},
+ "__pregel_writes": AnyList(),
"checkpoint_id": None,
"checkpoint_map": {
"": history[1].config["configurable"]["checkpoint_id"]
@@ -521,7 +525,8 @@ async def test_subgraph_w_interrupt(
"__pregel_resuming": True,
"__pregel_store": None,
"__pregel_task_id": history[1].tasks[0].id,
- "__pregel_resume_value": None,
+ "__pregel_scratchpad": {},
+ "__pregel_writes": AnyList(),
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[1].config["configurable"]["checkpoint_id"]
@@ -637,7 +642,8 @@ async def test_subgraph_w_interrupt(
"__pregel_resuming": True,
"__pregel_store": None,
"__pregel_task_id": history[1].tasks[0].id,
- "__pregel_resume_value": None,
+ "__pregel_scratchpad": {},
+ "__pregel_writes": AnyList(),
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[1].config["configurable"]["checkpoint_id"]
diff --git a/libs/scheduler-kafka/tests/test_subgraph_sync.py b/libs/scheduler-kafka/tests/test_subgraph_sync.py
index 75b9d6e73..5fa43998a 100644
--- a/libs/scheduler-kafka/tests/test_subgraph_sync.py
+++ b/libs/scheduler-kafka/tests/test_subgraph_sync.py
@@ -15,7 +15,7 @@ from langgraph.pregel import Pregel
from langgraph.scheduler.kafka import serde
from langgraph.scheduler.kafka.default_sync import DefaultProducer
from langgraph.scheduler.kafka.types import MessageToOrchestrator, Topics
-from tests.any import AnyDict
+from tests.any import AnyDict, AnyList
from tests.drain import drain_topics
from tests.messages import _AnyIdAIMessage, _AnyIdHumanMessage
@@ -195,7 +195,8 @@ def test_subgraph_w_interrupt(
"__pregel_resuming": False,
"__pregel_store": None,
"__pregel_task_id": history[0].tasks[0].id,
- "__pregel_resume_value": None,
+ "__pregel_scratchpad": {},
+ "__pregel_writes": AnyList(),
"checkpoint_id": None,
"checkpoint_map": {
"": history[0].config["configurable"]["checkpoint_id"]
@@ -260,7 +261,8 @@ def test_subgraph_w_interrupt(
"__pregel_dedupe_tasks": True,
"__pregel_resuming": False,
"__pregel_task_id": history[0].tasks[0].id,
- "__pregel_resume_value": None,
+ "__pregel_scratchpad": {},
+ "__pregel_writes": AnyList(),
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[0].config["configurable"]["checkpoint_id"]
@@ -355,7 +357,8 @@ def test_subgraph_w_interrupt(
"__pregel_store": None,
"__pregel_resuming": False,
"__pregel_task_id": history[0].tasks[0].id,
- "__pregel_resume_value": None,
+ "__pregel_scratchpad": {},
+ "__pregel_writes": AnyList(),
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[0].config["configurable"]["checkpoint_id"]
@@ -459,7 +462,8 @@ def test_subgraph_w_interrupt(
"__pregel_store": None,
"__pregel_resuming": True,
"__pregel_task_id": history[1].tasks[0].id,
- "__pregel_resume_value": None,
+ "__pregel_scratchpad": {},
+ "__pregel_writes": AnyList(),
"checkpoint_id": None,
"checkpoint_map": {
"": history[1].config["configurable"]["checkpoint_id"]
@@ -519,7 +523,8 @@ def test_subgraph_w_interrupt(
"__pregel_store": None,
"__pregel_resuming": True,
"__pregel_task_id": history[1].tasks[0].id,
- "__pregel_resume_value": None,
+ "__pregel_scratchpad": {},
+ "__pregel_writes": AnyList(),
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[1].config["configurable"]["checkpoint_id"]
@@ -635,7 +640,8 @@ def test_subgraph_w_interrupt(
"__pregel_resuming": True,
"__pregel_store": None,
"__pregel_task_id": history[1].tasks[0].id,
- "__pregel_resume_value": None,
+ "__pregel_scratchpad": {},
+ "__pregel_writes": AnyList(),
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
"checkpoint_map": {
"": history[1].config["configurable"]["checkpoint_id"]
From d457ad3cc272b8884a3a9e6cd829323748cc8ae5 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 4 Dec 2024 14:50:23 -0800
Subject: [PATCH 129/149] Clean up code snippet (#2637)
---
docs/docs/concepts/persistence.md | 5 ++++-
docs/docs/how-tos/memory/semantic-search.ipynb | 1 -
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/docs/docs/concepts/persistence.md b/docs/docs/concepts/persistence.md
index 8f4aa993a..0ec126316 100644
--- a/docs/docs/concepts/persistence.md
+++ b/docs/docs/concepts/persistence.md
@@ -276,9 +276,11 @@ The attributes it has are:
Beyond simple retrieval, the store also supports semantic search, allowing you to find memories based on meaning rather than exact matches. To enable this, configure the store with an embedding model:
```python
+from langchain.embeddings import init_embeddings
+
store = InMemoryStore(
index={
- "embed": "openai:text-embedding-3-small", # Embedding provider
+ "embed": init_embeddings("openai:text-embedding-3-small"), # Embedding provider
"dims": 1536, # Embedding dimensions
"fields": ["food_preference", "$"] # Fields to embed
}
@@ -289,6 +291,7 @@ Now when searching, you can use natural language queries to find relevant memori
```python
# Find memories about food preferences
+# (This can be done after putting memories into the store)
memories = store.search(
namespace_for_memory,
query="What does the user like to eat?",
diff --git a/docs/docs/how-tos/memory/semantic-search.ipynb b/docs/docs/how-tos/memory/semantic-search.ipynb
index 24905e625..658e4bb29 100644
--- a/docs/docs/how-tos/memory/semantic-search.ipynb
+++ b/docs/docs/how-tos/memory/semantic-search.ipynb
@@ -297,7 +297,6 @@
}
],
"source": [
- "embeddings = init_embeddings(\"openai:text-embedding-3-small\")\n",
"store = InMemoryStore(\n",
" index={\n",
" \"embed\": embeddings,\n",
From b4b3ac6f57adad2b0458026622c1e0ceb07c6c9f Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 4 Dec 2024 15:12:03 -0800
Subject: [PATCH 130/149] lib: Merge GraphCommand and Command
- Now we have only Command
- Command(goto=) combines the previous functionality of Command(send=) and Command(goto=)
---
libs/langgraph/langgraph/graph/__init__.py | 3 +-
libs/langgraph/langgraph/graph/state.py | 48 ++++----------
libs/langgraph/langgraph/pregel/io.py | 9 +--
libs/langgraph/langgraph/types.py | 2 +-
libs/langgraph/tests/test_pregel.py | 58 ++++++++---------
libs/langgraph/tests/test_pregel_async.py | 68 ++++++++++----------
libs/scheduler-kafka/tests/test_push.py | 18 +++---
libs/scheduler-kafka/tests/test_push_sync.py | 18 +++---
libs/sdk-py/langgraph_sdk/schema.py | 2 +-
9 files changed, 101 insertions(+), 125 deletions(-)
diff --git a/libs/langgraph/langgraph/graph/__init__.py b/libs/langgraph/langgraph/graph/__init__.py
index 241106a3a..c81ad9903 100644
--- a/libs/langgraph/langgraph/graph/__init__.py
+++ b/libs/langgraph/langgraph/graph/__init__.py
@@ -1,13 +1,12 @@
from langgraph.graph.graph import END, START, Graph
from langgraph.graph.message import MessageGraph, MessagesState, add_messages
-from langgraph.graph.state import GraphCommand, StateGraph
+from langgraph.graph.state import StateGraph
__all__ = [
"END",
"START",
"Graph",
"StateGraph",
- "GraphCommand",
"MessageGraph",
"add_messages",
"MessagesState",
diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py
index 1a7208a2a..e63f25111 100644
--- a/libs/langgraph/langgraph/graph/state.py
+++ b/libs/langgraph/langgraph/graph/state.py
@@ -1,4 +1,3 @@
-import dataclasses
import inspect
import logging
import typing
@@ -9,7 +8,6 @@ from types import FunctionType
from typing import (
Any,
Callable,
- Generic,
Literal,
NamedTuple,
Optional,
@@ -55,7 +53,7 @@ from langgraph.managed.base import (
from langgraph.pregel.read import ChannelRead, PregelNode
from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry
from langgraph.store.base import BaseStore
-from langgraph.types import _DC_KWARGS, All, Checkpointer, Command, N, RetryPolicy
+from langgraph.types import All, Checkpointer, Command, RetryPolicy
from langgraph.utils.fields import get_field_default
from langgraph.utils.pydantic import create_model
from langgraph.utils.runnable import RunnableCallable, coerce_to_runnable
@@ -84,22 +82,6 @@ def _get_node_name(node: RunnableLike) -> str:
raise TypeError(f"Unsupported node type: {type(node)}")
-@dataclasses.dataclass(**_DC_KWARGS)
-class GraphCommand(Generic[N], Command[N]):
- """One or more commands to update a StateGraph's state and go to, or send messages to nodes."""
-
- goto: Union[str, Sequence[str]] = ()
-
- def __repr__(self) -> str:
- # get all non-None values
- contents = ", ".join(
- f"{key}={value!r}"
- for key, value in dataclasses.asdict(self).items()
- if value
- )
- return f"Command({contents})"
-
-
class StateNodeSpec(NamedTuple):
runnable: Runnable
metadata: Optional[dict[str, Any]]
@@ -392,7 +374,7 @@ class StateGraph(Graph):
input = input_hint
if (
(rtn := hints.get("return"))
- and get_origin(rtn) in (Command, GraphCommand)
+ and get_origin(rtn) is Command
and (rargs := get_args(rtn))
and get_origin(rargs[0]) is Literal
and (vals := get_args(rargs[0]))
@@ -834,15 +816,12 @@ def _control_branch(value: Any) -> Sequence[Union[str, Send]]:
if value.graph == Command.PARENT:
raise ParentCommand(value)
rtn: list[Union[str, Send]] = []
- if isinstance(value, GraphCommand):
- if isinstance(value.goto, str):
- rtn.append(value.goto)
- else:
- rtn.extend(value.goto)
- if isinstance(value.send, Send):
- rtn.append(value.send)
+ if isinstance(value.goto, Send):
+ rtn.append(value.goto)
+ elif isinstance(value.goto, str):
+ rtn.append(value.goto)
else:
- rtn.extend(value.send)
+ rtn.extend(value.goto)
return rtn
@@ -854,15 +833,12 @@ async def _acontrol_branch(value: Any) -> Sequence[Union[str, Send]]:
if value.graph == Command.PARENT:
raise ParentCommand(value)
rtn: list[Union[str, Send]] = []
- if isinstance(value, GraphCommand):
- if isinstance(value.goto, str):
- rtn.append(value.goto)
- else:
- rtn.extend(value.goto)
- if isinstance(value.send, Send):
- rtn.append(value.send)
+ if isinstance(value.goto, Send):
+ rtn.append(value.goto)
+ elif isinstance(value.goto, str):
+ rtn.append(value.goto)
else:
- rtn.extend(value.send)
+ rtn.extend(value.goto)
return rtn
diff --git a/libs/langgraph/langgraph/pregel/io.py b/libs/langgraph/langgraph/pregel/io.py
index ed2c28938..c1fed349a 100644
--- a/libs/langgraph/langgraph/pregel/io.py
+++ b/libs/langgraph/langgraph/pregel/io.py
@@ -72,17 +72,18 @@ def map_command(
"""Map input chunk to a sequence of pending writes in the form (channel, value)."""
if cmd.graph == Command.PARENT:
raise InvalidUpdateError("There is not parent graph")
- if cmd.send:
+ if cmd.goto:
if isinstance(cmd.send, (tuple, list)):
- sends = cmd.send
+ sends = cmd.goto
else:
- sends = [cmd.send]
+ sends = [cmd.goto]
for send in sends:
if not isinstance(send, Send):
raise TypeError(
- f"In Command.send, expected Send, got {type(send).__name__}"
+ f"In Command.goto, expected Send, got {type(send).__name__}"
)
yield (NULL_TASK_ID, PUSH if FF_SEND_V2 else TASKS, send)
+ # TODO handle goto str for state graph
if cmd.resume:
if isinstance(cmd.resume, dict) and all(is_task_id(k) for k in cmd.resume):
for tid, resume in cmd.resume.items():
diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py
index 4047a28f7..67c7e53f8 100644
--- a/libs/langgraph/langgraph/types.py
+++ b/libs/langgraph/langgraph/types.py
@@ -249,8 +249,8 @@ class Command(Generic[N]):
graph: Optional[str] = None
update: Optional[dict[str, Any]] = None
- send: Union[Send, Sequence[Send]] = ()
resume: Optional[Union[Any, dict[str, Any]]] = None
+ goto: Union[Send, Sequence[Union[Send, str]], str] = ()
def __repr__(self) -> str:
# get all non-None values
diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py
index 4f768badd..68071594b 100644
--- a/libs/langgraph/tests/test_pregel.py
+++ b/libs/langgraph/tests/test_pregel.py
@@ -65,7 +65,7 @@ from langgraph.constants import (
START,
)
from langgraph.errors import InvalidUpdateError, MultipleSubgraphsError, NodeInterrupt
-from langgraph.graph import END, Graph, GraphCommand, StateGraph
+from langgraph.graph import END, Graph, StateGraph
from langgraph.graph.message import MessageGraph, MessagesState, add_messages
from langgraph.managed.shared_value import SharedValue
from langgraph.prebuilt.chat_agent_executor import create_tool_calling_executor
@@ -270,10 +270,10 @@ def test_graph_validation_with_command() -> None:
bar: str
def node_a(state: State):
- return GraphCommand(goto="b", update={"foo": "bar"})
+ return Command(goto="b", update={"foo": "bar"})
def node_b(state: State):
- return GraphCommand(goto=END, update={"bar": "baz"})
+ return Command(goto=END, update={"bar": "baz"})
builder = StateGraph(State)
builder.add_node("a", node_a)
@@ -1925,8 +1925,8 @@ def test_send_sequences() -> None:
def send_for_fun(state):
return [
- Send("2", Command(send=Send("2", 3))),
- Send("2", GraphCommand(send=Send("2", 4))),
+ Send("2", Command(goto=Send("2", 3))),
+ Send("2", Command(goto=Send("2", 4))),
"3.1",
]
@@ -1947,8 +1947,8 @@ def test_send_sequences() -> None:
== [
"0",
"1",
- "2|Command(send=Send(node='2', arg=3))",
- "2|Command(send=Send(node='2', arg=4))",
+ "2|Command(goto=Send(node='2', arg=3))",
+ "2|Command(goto=Send(node='2', arg=4))",
"2|3",
"2|4",
"3",
@@ -1959,8 +1959,8 @@ def test_send_sequences() -> None:
"0",
"1",
"3.1",
- "2|Command(send=Send(node='2', arg=3))",
- "2|Command(send=Send(node='2', arg=4))",
+ "2|Command(goto=Send(node='2', arg=3))",
+ "2|Command(goto=Send(node='2', arg=4))",
"3",
"2|3",
"2|4",
@@ -2000,15 +2000,15 @@ def test_send_dedupe_on_resume(
if isinstance(state, list)
else ["|".join((self.name, str(state)))]
)
- if isinstance(state, GraphCommand):
+ if isinstance(state, Command):
return replace(state, update=update)
else:
return update
def send_for_fun(state):
return [
- Send("2", GraphCommand(send=Send("2", 3))),
- Send("2", GraphCommand(send=Send("flaky", 4))),
+ Send("2", Command(goto=Send("2", 3))),
+ Send("2", Command(goto=Send("flaky", 4))),
"3.1",
]
@@ -2030,8 +2030,8 @@ def test_send_dedupe_on_resume(
assert graph.invoke(["0"], thread1, debug=1) == [
"0",
"1",
- "2|Command(send=Send(node='2', arg=3))",
- "2|Command(send=Send(node='flaky', arg=4))",
+ "2|Command(goto=Send(node='2', arg=3))",
+ "2|Command(goto=Send(node='flaky', arg=4))",
"2|3",
]
assert builder.nodes["2"].runnable.func.ticks == 3
@@ -2046,8 +2046,8 @@ def test_send_dedupe_on_resume(
assert graph.invoke(None, thread1, debug=1) == [
"0",
"1",
- "2|Command(send=Send(node='2', arg=3))",
- "2|Command(send=Send(node='flaky', arg=4))",
+ "2|Command(goto=Send(node='2', arg=3))",
+ "2|Command(goto=Send(node='flaky', arg=4))",
"2|3",
"flaky|4",
"3",
@@ -2069,8 +2069,8 @@ def test_send_dedupe_on_resume(
values=[
"0",
"1",
- "2|Command(send=Send(node='2', arg=3))",
- "2|Command(send=Send(node='flaky', arg=4))",
+ "2|Command(goto=Send(node='2', arg=3))",
+ "2|Command(goto=Send(node='flaky', arg=4))",
"2|3",
"flaky|4",
"3",
@@ -2105,8 +2105,8 @@ def test_send_dedupe_on_resume(
values=[
"0",
"1",
- "2|Command(send=Send(node='2', arg=3))",
- "2|Command(send=Send(node='flaky', arg=4))",
+ "2|Command(goto=Send(node='2', arg=3))",
+ "2|Command(goto=Send(node='flaky', arg=4))",
"2|3",
"flaky|4",
],
@@ -2123,8 +2123,8 @@ def test_send_dedupe_on_resume(
"writes": {
"1": ["1"],
"2": [
- ["2|Command(send=Send(node='2', arg=3))"],
- ["2|Command(send=Send(node='flaky', arg=4))"],
+ ["2|Command(goto=Send(node='2', arg=3))"],
+ ["2|Command(goto=Send(node='flaky', arg=4))"],
["2|3"],
],
"flaky": ["flaky|4"],
@@ -2209,7 +2209,7 @@ def test_send_dedupe_on_resume(
error=None,
interrupts=(),
state=None,
- result=["2|Command(send=Send(node='2', arg=3))"],
+ result=["2|Command(goto=Send(node='2', arg=3))"],
),
PregelTask(
id=AnyStr(),
@@ -2223,7 +2223,7 @@ def test_send_dedupe_on_resume(
error=None,
interrupts=(),
state=None,
- result=["2|Command(send=Send(node='flaky', arg=4))"],
+ result=["2|Command(goto=Send(node='flaky', arg=4))"],
),
PregelTask(
id=AnyStr(),
@@ -2786,10 +2786,10 @@ def test_send_react_interrupt_control(
tool_calls=[ToolCall(name="foo", args={"hi": [1, 2, 3]}, id=AnyStr())],
)
- def agent(state) -> GraphCommand[Literal["foo"]]:
- return GraphCommand(
+ def agent(state) -> Command[Literal["foo"]]:
+ return Command(
update={"messages": ai_message},
- send=[Send(call["name"], call) for call in ai_message.tool_calls],
+ goto=[Send(call["name"], call) for call in ai_message.tool_calls],
)
foo_called = 0
@@ -14580,9 +14580,9 @@ def test_parent_command(request: pytest.FixtureRequest, checkpointer_name: str)
from langchain_core.tools import tool
@tool(return_direct=True)
- def get_user_name() -> GraphCommand:
+ def get_user_name() -> Command:
"""Retrieve user name"""
- return GraphCommand(update={"user_name": "Meow"}, graph=GraphCommand.PARENT)
+ return Command(update={"user_name": "Meow"}, graph=Command.PARENT)
subgraph_builder = StateGraph(MessagesState)
subgraph_builder.add_node("tool", get_user_name)
diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py
index addb18b2b..538730c78 100644
--- a/libs/langgraph/tests/test_pregel_async.py
+++ b/libs/langgraph/tests/test_pregel_async.py
@@ -62,7 +62,7 @@ from langgraph.constants import (
START,
)
from langgraph.errors import InvalidUpdateError, MultipleSubgraphsError, NodeInterrupt
-from langgraph.graph import END, Graph, GraphCommand, StateGraph
+from langgraph.graph import END, Graph, StateGraph
from langgraph.graph.message import MessageGraph, MessagesState, add_messages
from langgraph.managed.shared_value import SharedValue
from langgraph.prebuilt.chat_agent_executor import create_tool_calling_executor
@@ -2580,8 +2580,8 @@ async def test_send_sequences(checkpointer_name: str) -> None:
async def send_for_fun(state):
return [
- Send("2", Command(send=Send("2", 3))),
- Send("2", GraphCommand(send=Send("2", 4))),
+ Send("2", Command(goto=Send("2", 3))),
+ Send("2", Command(goto=Send("2", 4))),
"3.1",
]
@@ -2602,8 +2602,8 @@ async def test_send_sequences(checkpointer_name: str) -> None:
== [
"0",
"1",
- "2|Command(send=Send(node='2', arg=3))",
- "2|Command(send=Send(node='2', arg=4))",
+ "2|Command(goto=Send(node='2', arg=3))",
+ "2|Command(goto=Send(node='2', arg=4))",
"2|3",
"2|4",
"3",
@@ -2614,8 +2614,8 @@ async def test_send_sequences(checkpointer_name: str) -> None:
"0",
"1",
"3.1",
- "2|Command(send=Send(node='2', arg=3))",
- "2|Command(send=Send(node='2', arg=4))",
+ "2|Command(goto=Send(node='2', arg=3))",
+ "2|Command(goto=Send(node='2', arg=4))",
"3",
"2|3",
"2|4",
@@ -2632,16 +2632,16 @@ async def test_send_sequences(checkpointer_name: str) -> None:
assert await graph.ainvoke(["0"], thread1) == [
"0",
"1",
- "2|Command(send=Send(node='2', arg=3))",
- "2|Command(send=Send(node='2', arg=4))",
+ "2|Command(goto=Send(node='2', arg=3))",
+ "2|Command(goto=Send(node='2', arg=4))",
"2|3",
"2|4",
]
assert await graph.ainvoke(None, thread1) == [
"0",
"1",
- "2|Command(send=Send(node='2', arg=3))",
- "2|Command(send=Send(node='2', arg=4))",
+ "2|Command(goto=Send(node='2', arg=3))",
+ "2|Command(goto=Send(node='2', arg=4))",
"2|3",
"2|4",
"3",
@@ -2677,15 +2677,15 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
if isinstance(state, list)
else ["|".join((self.name, str(state)))]
)
- if isinstance(state, GraphCommand):
+ if isinstance(state, Command):
return replace(state, update=update)
else:
return update
def send_for_fun(state):
return [
- Send("2", GraphCommand(send=Send("2", 3))),
- Send("2", GraphCommand(send=Send("flaky", 4))),
+ Send("2", Command(goto=Send("2", 3))),
+ Send("2", Command(goto=Send("flaky", 4))),
"3.1",
]
@@ -2708,8 +2708,8 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
assert await graph.ainvoke(["0"], thread1, debug=1) == [
"0",
"1",
- "2|Command(send=Send(node='2', arg=3))",
- "2|Command(send=Send(node='flaky', arg=4))",
+ "2|Command(goto=Send(node='2', arg=3))",
+ "2|Command(goto=Send(node='flaky', arg=4))",
"2|3",
]
assert builder.nodes["2"].runnable.func.ticks == 3
@@ -2718,8 +2718,8 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
assert await graph.ainvoke(None, thread1, debug=1) == [
"0",
"1",
- "2|Command(send=Send(node='2', arg=3))",
- "2|Command(send=Send(node='flaky', arg=4))",
+ "2|Command(goto=Send(node='2', arg=3))",
+ "2|Command(goto=Send(node='flaky', arg=4))",
"2|3",
"flaky|4",
"3",
@@ -2736,8 +2736,8 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
values=[
"0",
"1",
- "2|Command(send=Send(node='2', arg=3))",
- "2|Command(send=Send(node='flaky', arg=4))",
+ "2|Command(goto=Send(node='2', arg=3))",
+ "2|Command(goto=Send(node='flaky', arg=4))",
"2|3",
"flaky|4",
"3",
@@ -2772,8 +2772,8 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
values=[
"0",
"1",
- "2|Command(send=Send(node='2', arg=3))",
- "2|Command(send=Send(node='flaky', arg=4))",
+ "2|Command(goto=Send(node='2', arg=3))",
+ "2|Command(goto=Send(node='flaky', arg=4))",
"2|3",
"flaky|4",
],
@@ -2790,8 +2790,8 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
"writes": {
"1": ["1"],
"2": [
- ["2|Command(send=Send(node='2', arg=3))"],
- ["2|Command(send=Send(node='flaky', arg=4))"],
+ ["2|Command(goto=Send(node='2', arg=3))"],
+ ["2|Command(goto=Send(node='flaky', arg=4))"],
["2|3"],
],
"flaky": ["flaky|4"],
@@ -2876,7 +2876,7 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
error=None,
interrupts=(),
state=None,
- result=["2|Command(send=Send(node='2', arg=3))"],
+ result=["2|Command(goto=Send(node='2', arg=3))"],
),
PregelTask(
id=AnyStr(),
@@ -2890,7 +2890,7 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
error=None,
interrupts=(),
state=None,
- result=["2|Command(send=Send(node='flaky', arg=4))"],
+ result=["2|Command(goto=Send(node='flaky', arg=4))"],
),
PregelTask(
id=AnyStr(),
@@ -3448,9 +3448,9 @@ async def test_send_react_interrupt_control(
)
async def agent(state) -> Command[Literal["foo"]]:
- return GraphCommand(
+ return Command(
update={"messages": ai_message},
- send=[Send(call["name"], call) for call in ai_message.tool_calls],
+ goto=[Send(call["name"], call) for call in ai_message.tool_calls],
)
foo_called = 0
@@ -3761,13 +3761,13 @@ async def test_max_concurrency(checkpointer_name: str) -> None:
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
async def test_max_concurrency_control(checkpointer_name: str) -> None:
- async def node1(state) -> GraphCommand[Literal["2"]]:
- return GraphCommand(update=["1"], send=[Send("2", idx) for idx in range(100)])
+ async def node1(state) -> Command[Literal["2"]]:
+ return Command(update=["1"], goto=[Send("2", idx) for idx in range(100)])
node2_currently = 0
node2_max_currently = 0
- async def node2(state) -> GraphCommand[Literal["3"]]:
+ async def node2(state) -> Command[Literal["3"]]:
nonlocal node2_currently, node2_max_currently
node2_currently += 1
if node2_currently > node2_max_currently:
@@ -3775,7 +3775,7 @@ async def test_max_concurrency_control(checkpointer_name: str) -> None:
await asyncio.sleep(0.1)
node2_currently -= 1
- return GraphCommand(update=[state], goto="3")
+ return Command(update=[state], goto="3")
async def node3(state) -> Literal["3"]:
return ["3"]
@@ -12788,9 +12788,9 @@ async def test_parent_command(checkpointer_name: str) -> None:
from langchain_core.tools import tool
@tool(return_direct=True)
- def get_user_name() -> GraphCommand:
+ def get_user_name() -> Command:
"""Retrieve user name"""
- return GraphCommand(update={"user_name": "Meow"}, graph=GraphCommand.PARENT)
+ return Command(update={"user_name": "Meow"}, graph=Command.PARENT)
subgraph_builder = StateGraph(MessagesState)
subgraph_builder.add_node("tool", get_user_name)
diff --git a/libs/scheduler-kafka/tests/test_push.py b/libs/scheduler-kafka/tests/test_push.py
index 15e9211a2..3d2e4d43d 100644
--- a/libs/scheduler-kafka/tests/test_push.py
+++ b/libs/scheduler-kafka/tests/test_push.py
@@ -11,10 +11,10 @@ from aiokafka import AIOKafkaProducer
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.constants import FF_SEND_V2, START
from langgraph.errors import NodeInterrupt
-from langgraph.graph.state import CompiledStateGraph, GraphCommand, StateGraph
+from langgraph.graph.state import CompiledStateGraph, StateGraph
from langgraph.scheduler.kafka import serde
from langgraph.scheduler.kafka.types import MessageToOrchestrator, Topics
-from langgraph.types import Send
+from langgraph.types import Command, Send
from tests.any import AnyDict
from tests.drain import drain_topics_async
@@ -48,15 +48,15 @@ def mk_push_graph(
if isinstance(state, list)
else ["|".join((self.name, str(state)))]
)
- if isinstance(state, GraphCommand):
+ if isinstance(state, Command):
return state.copy(update=update)
else:
return update
def send_for_fun(state):
return [
- Send("2", GraphCommand(send=Send("2", 3))),
- Send("2", GraphCommand(send=Send("flaky", 4))),
+ Send("2", Command(goto=Send("2", 3))),
+ Send("2", Command(goto=Send("flaky", 4))),
"3.1",
]
@@ -105,8 +105,8 @@ async def test_push_graph(topics: Topics, acheckpointer: BaseCheckpointSaver) ->
== [
"0",
"1",
- "2|Control(send=Send(node='2', arg=3))",
- "2|Control(send=Send(node='flaky', arg=4))",
+ "2|Control(goto=Send(node='2', arg=3))",
+ "2|Control(goto=Send(node='flaky', arg=4))",
"2|3",
]
)
@@ -182,8 +182,8 @@ async def test_push_graph(topics: Topics, acheckpointer: BaseCheckpointSaver) ->
== [
"0",
"1",
- "2|Control(send=Send(node='2', arg=3))",
- "2|Control(send=Send(node='flaky', arg=4))",
+ "2|Control(goto=Send(node='2', arg=3))",
+ "2|Control(goto=Send(node='flaky', arg=4))",
"2|3",
"flaky|4",
"3",
diff --git a/libs/scheduler-kafka/tests/test_push_sync.py b/libs/scheduler-kafka/tests/test_push_sync.py
index 27cd96cb7..ee33d613e 100644
--- a/libs/scheduler-kafka/tests/test_push_sync.py
+++ b/libs/scheduler-kafka/tests/test_push_sync.py
@@ -10,11 +10,11 @@ import pytest
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.constants import FF_SEND_V2, START
from langgraph.errors import NodeInterrupt
-from langgraph.graph.state import CompiledStateGraph, GraphCommand, StateGraph
+from langgraph.graph.state import CompiledStateGraph, StateGraph
from langgraph.scheduler.kafka import serde
from langgraph.scheduler.kafka.default_sync import DefaultProducer
from langgraph.scheduler.kafka.types import MessageToOrchestrator, Topics
-from langgraph.types import Send
+from langgraph.types import Command, Send
from tests.any import AnyDict
from tests.drain import drain_topics
@@ -48,15 +48,15 @@ def mk_push_graph(
if isinstance(state, list)
else ["|".join((self.name, str(state)))]
)
- if isinstance(state, GraphCommand):
+ if isinstance(state, Command):
return state.copy(update=update)
else:
return update
def send_for_fun(state):
return [
- Send("2", GraphCommand(send=Send("2", 3))),
- Send("2", GraphCommand(send=Send("flaky", 4))),
+ Send("2", Command(goto=Send("2", 3))),
+ Send("2", Command(goto=Send("flaky", 4))),
"3.1",
]
@@ -106,8 +106,8 @@ def test_push_graph(topics: Topics, acheckpointer: BaseCheckpointSaver) -> None:
== [
"0",
"1",
- "2|Control(send=Send(node='2', arg=3))",
- "2|Control(send=Send(node='flaky', arg=4))",
+ "2|Control(goto=Send(node='2', arg=3))",
+ "2|Control(goto=Send(node='flaky', arg=4))",
"2|3",
]
)
@@ -184,8 +184,8 @@ def test_push_graph(topics: Topics, acheckpointer: BaseCheckpointSaver) -> None:
== [
"0",
"1",
- "2|Control(send=Send(node='2', arg=3))",
- "2|Control(send=Send(node='flaky', arg=4))",
+ "2|Control(goto=Send(node='2', arg=3))",
+ "2|Control(goto=Send(node='flaky', arg=4))",
"2|3",
"flaky|4",
"3",
diff --git a/libs/sdk-py/langgraph_sdk/schema.py b/libs/sdk-py/langgraph_sdk/schema.py
index 1ccae3e89..6237ea5bd 100644
--- a/libs/sdk-py/langgraph_sdk/schema.py
+++ b/libs/sdk-py/langgraph_sdk/schema.py
@@ -373,6 +373,6 @@ class Send(TypedDict):
class Command(TypedDict, total=False):
- send: Union[Send, Sequence[Send]]
+ goto: Union[Send, str, Sequence[Union[Send, str]]]
update: dict[str, Any]
resume: Any
From df70e91daecac6b7d2b187b7c67a5c725e7dbe11 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 4 Dec 2024 15:13:55 -0800
Subject: [PATCH 131/149] Lint
---
libs/langgraph/langgraph/pregel/io.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/langgraph/langgraph/pregel/io.py b/libs/langgraph/langgraph/pregel/io.py
index c1fed349a..b2596d3ad 100644
--- a/libs/langgraph/langgraph/pregel/io.py
+++ b/libs/langgraph/langgraph/pregel/io.py
@@ -73,7 +73,7 @@ def map_command(
if cmd.graph == Command.PARENT:
raise InvalidUpdateError("There is not parent graph")
if cmd.goto:
- if isinstance(cmd.send, (tuple, list)):
+ if isinstance(cmd.goto, (tuple, list)):
sends = cmd.goto
else:
sends = [cmd.goto]
From 771b9b28cd78a8bebae4e115c07c588d0436efd1 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 4 Dec 2024 15:29:51 -0800
Subject: [PATCH 132/149] Speed up tests
---
.github/workflows/_test_langgraph.yml | 2 +-
libs/langgraph/Makefile | 6 ++++++
libs/langgraph/tests/test_pregel.py | 1 -
libs/langgraph/tests/test_pregel_async.py | 3 ---
4 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/_test_langgraph.yml b/.github/workflows/_test_langgraph.yml
index 5c3f5182e..2708d0f23 100644
--- a/.github/workflows/_test_langgraph.yml
+++ b/.github/workflows/_test_langgraph.yml
@@ -60,7 +60,7 @@ jobs:
env:
LANGGRAPH_FF_SEND_V2: ${{ matrix.ff-send-v2 }}
run: |
- make test
+ make test_parallel
- name: Ensure the tests did not create any additional files
shell: bash
diff --git a/libs/langgraph/Makefile b/libs/langgraph/Makefile
index 43d0c7afe..8974fcd32 100644
--- a/libs/langgraph/Makefile
+++ b/libs/langgraph/Makefile
@@ -48,6 +48,12 @@ test:
make stop-postgres; \
exit $$EXIT_CODE
+test_parallel:
+ make start-postgres && poetry run pytest -n auto --dist worksteal $(TEST); \
+ EXIT_CODE=$$?; \
+ make stop-postgres; \
+ exit $$EXIT_CODE
+
WORKERS ?= auto
XDIST_ARGS := $(if $(WORKERS),-n $(WORKERS) --dist worksteal,)
MAXFAIL ?=
diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py
index 68071594b..f69d36ed3 100644
--- a/libs/langgraph/tests/test_pregel.py
+++ b/libs/langgraph/tests/test_pregel.py
@@ -1969,7 +1969,6 @@ def test_send_sequences() -> None:
)
-@pytest.mark.repeat(20)
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
def test_send_dedupe_on_resume(
request: pytest.FixtureRequest, checkpointer_name: str
diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py
index 538730c78..514703781 100644
--- a/libs/langgraph/tests/test_pregel_async.py
+++ b/libs/langgraph/tests/test_pregel_async.py
@@ -847,7 +847,6 @@ async def test_node_not_cancelled_on_other_node_interrupted(
assert awhiles == 1
-@pytest.mark.repeat(10)
async def test_step_timeout_on_stream_hang() -> None:
inner_task_cancelled = False
@@ -2559,7 +2558,6 @@ async def test_concurrent_emit_sends() -> None:
)
-@pytest.mark.repeat(10)
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
async def test_send_sequences(checkpointer_name: str) -> None:
class Node:
@@ -2649,7 +2647,6 @@ async def test_send_sequences(checkpointer_name: str) -> None:
]
-@pytest.mark.repeat(20)
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
async def test_send_dedupe_on_resume(checkpointer_name: str) -> None:
if not FF_SEND_V2:
From 7a326ef7688ce80a3517f234a084b03dd75f8e1e Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 4 Dec 2024 15:44:24 -0800
Subject: [PATCH 133/149] lib 0.2.55
---
libs/langgraph/pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml
index e66600939..cee26320e 100644
--- a/libs/langgraph/pyproject.toml
+++ b/libs/langgraph/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
-version = "0.2.54"
+version = "0.2.55"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
From dad0f39fa47ed1e77ca461c39357511bb29d930f Mon Sep 17 00:00:00 2001
From: Eugene Yurtsev
Date: Wed, 4 Dec 2024 18:49:17 -0500
Subject: [PATCH 134/149] concepts: reword network architecture (#2625)
---
docs/docs/concepts/multi_agent.md | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/docs/docs/concepts/multi_agent.md b/docs/docs/concepts/multi_agent.md
index 46bf4eeda..d8ef0a73b 100644
--- a/docs/docs/concepts/multi_agent.md
+++ b/docs/docs/concepts/multi_agent.md
@@ -28,12 +28,7 @@ There are several ways to connect agents in a multi-agent system:
### Network
-In this architecture, agents are defined as graph nodes. Each agent can communicate with every other agent (many-to-many connections) and can decide which agent to call next. While very flexible, this architecture doesn't scale well as the number of agents grows:
-
-- hard to enforce which agent should be called next
-- hard to determine how much [information](#shared-message-list) should be passed between the agents
-
-We recommend avoiding this architecture in production and using one of the below architectures instead.
+In this architecture, agents are defined as graph nodes. Each agent can communicate with every other agent (many-to-many connections) and can decide which agent to call next. This architecture is good for problems that do not have a clear hierarchy of agents or a specific sequence in which agents should be called.
### Supervisor
From 257e44ccb40b6efe943f007d93221e22aa75cb53 Mon Sep 17 00:00:00 2001
From: vbarda
Date: Wed, 4 Dec 2024 18:46:29 -0500
Subject: [PATCH 135/149] update
---
docs/docs/concepts/low_level.md | 32 ++++++++++++-------------
docs/docs/how-tos/graph-command.ipynb | 34 +++++++++++++--------------
libs/langgraph/langgraph/types.py | 6 ++++-
3 files changed, 38 insertions(+), 34 deletions(-)
diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md
index 75ff6d4a3..c775eae81 100644
--- a/docs/docs/concepts/low_level.md
+++ b/docs/docs/concepts/low_level.md
@@ -322,13 +322,13 @@ def continue_to_jokes(state: OverallState):
graph.add_conditional_edges("node_a", continue_to_jokes)
```
-## `GraphCommand`
+## `Command`
-It can be useful to combine control flow (edges) and state updates (nodes). For example, you might want to BOTH perform state updates AND decide which node to go to next in the SAME node. LangGraph provides a way to do so by returning a [`GraphCommand`][langgraph.graph.state.GraphCommand] object from node functions:
+It can be useful to combine control flow (edges) and state updates (nodes). For example, you might want to BOTH perform state updates AND decide which node to go to next in the SAME node. LangGraph provides a way to do so by returning a [`Command`][langgraph.graph.state.GraphCommand] object from node functions:
```python
-def my_node(state: State) -> GraphCommand[Literal["my_other_node"]]:
- return GraphCommand(
+def my_node(state: State) -> Command[Literal["my_other_node"]]:
+ return Command(
# state update
update={"foo": "bar"},
# control flow
@@ -336,25 +336,25 @@ def my_node(state: State) -> GraphCommand[Literal["my_other_node"]]:
)
```
-`GraphCommand` has the following properties:
+`Command` has the following properties:
| Property | Description |
| --- | --- |
-| `graph` | Graph to send the command to. Supported values:
- `None`: the current graph (default)
- `GraphCommand.PARENT`: parent graph |
-| `goto` | Name of the node to navigate to next. Can be any node that belongs to the specified `graph` (current or parent). If `goto` not specified, the graph will halt after executing the current superstep. |
+| `graph` | Graph to send the command to. Supported values:
- `None`: the current graph (default)
- `GraphCommand.PARENT`: closest parent graph |
| `update` | State update to apply to the graph's state at the current superstep |
-| `send` | List of [`Send`](#send) objects to send to other nodes |
| `resume` | Value to resume execution with. Will be used when `interrupt()` is called |
+| `goto` | Can be one of the following:
- name of the node to navigate to next (any node that belongs to the specified `graph`)
- list of node names to navigate to next
- `Send` object
- sequence of `Send` objects
If `goto` is not specified and there are no other tasks left in the graph, the graph will halt after executing the current superstep. |
```python
-from langgraph.graph import GraphCommand, StateGraph, START
+from langgraph.graph import StateGraph, START
+from langgraph.types import Command
from typing_extensions import Literal, TypedDict
class State(TypedDict):
foo: str
-def my_node(state: State) -> GraphCommand[Literal["my_other_node"]]:
- return GraphCommand(update={"foo": "bar"}, goto="my_other_node")
+def my_node(state: State) -> Command[Literal["my_other_node"]]:
+ return Command(update={"foo": "bar"}, goto="my_other_node")
def my_other_node(state: State):
return {"foo": state["foo"] + "baz"}
@@ -367,17 +367,17 @@ builder.add_node("my_other_node", my_other_node)
graph = builder.compile()
```
-With `GraphCommand` you can also achieve dynamic control flow behavior (identical to [conditional edges](#conditional-edges)):
+With `Command` you can also achieve dynamic control flow behavior (identical to [conditional edges](#conditional-edges)):
```python
-def my_node(state: State) -> GraphCommand[Literal["my_other_node", "__end__"]]:
+def my_node(state: State) -> Command[Literal["my_other_node", "__end__"]]:
if state["foo"] == "bar":
- return GraphCommand(update={"foo": "baz"}, goto="my_other_node")
+ return Command(update={"foo": "baz"}, goto="my_other_node")
else:
- return GraphCommand(goto="__end__")
+ return Command(goto="__end__")
```
-Check out this [how-to guide](../how-tos/graph-command.ipynb) for an end-to-end example of how to use `GraphCommand`.
+Check out this [how-to guide](../how-tos/graph-command.ipynb) for an end-to-end example of how to use `Command`.
## Persistence
diff --git a/docs/docs/how-tos/graph-command.ipynb b/docs/docs/how-tos/graph-command.ipynb
index b768e75a8..ad33e3a62 100644
--- a/docs/docs/how-tos/graph-command.ipynb
+++ b/docs/docs/how-tos/graph-command.ipynb
@@ -5,7 +5,7 @@
"id": "d33ecddc-6818-41a3-9d0d-b1b1cbcd286d",
"metadata": {},
"source": [
- "# How to combine control flow and state updates with GraphCommand"
+ "# How to combine control flow and state updates with Command"
]
},
{
@@ -19,12 +19,12 @@
" - [State](../../concepts/low_level/#state)\n",
" - [Nodes](../../concepts/low_level/#nodes)\n",
" - [Edges](../../concepts/low_level/#edges)\n",
- " - [GraphCommand](../../concepts/low_level/#graphcommand)\n",
+ " - [Command](../../concepts/low_level/#command)\n",
"\n",
- "It can be useful to combine control flow (edges) and state updates (nodes). For example, you might want to BOTH perform state updates AND decide which node to go to next in the SAME node. LangGraph provides a way to do so by returning a `GraphCommand` object from node functions:\n",
+ "It can be useful to combine control flow (edges) and state updates (nodes). For example, you might want to BOTH perform state updates AND decide which node to go to next in the SAME node. LangGraph provides a way to do so by returning a `Command` object from node functions:\n",
"\n",
"```python\n",
- "def my_node(state: State) -> GraphCommand[Literal[\"my_other_node\"]]:\n",
+ "def my_node(state: State) -> Command[Literal[\"my_other_node\"]]:\n",
" return GraphCommand(\n",
" # state update\n",
" update={\"foo\": \"bar\"},\n",
@@ -33,7 +33,7 @@
" )\n",
"```\n",
"\n",
- "This guide shows how you can do use `GraphCommand` to add dynamic control flow in your LangGraph app."
+ "This guide shows how you can do use `Command` to add dynamic control flow in your LangGraph app."
]
},
{
@@ -83,7 +83,7 @@
"id": "6a08d957-b3d2-4538-bf4a-68ef90a51b98",
"metadata": {},
"source": [
- "## Control flow with GraphCommand"
+ "## Control flow with Command"
]
},
{
@@ -96,7 +96,8 @@
"import random\n",
"from typing_extensions import TypedDict, Literal\n",
"\n",
- "from langgraph.graph import GraphCommand, StateGraph, START\n",
+ "from langgraph.graph import StateGraph, START\n",
+ "from langgraph.types import Command\n",
"\n",
"\n",
"# Define graph state\n",
@@ -107,7 +108,7 @@
"# Define the nodes\n",
"\n",
"\n",
- "def node_a(state: State) -> GraphCommand[Literal[\"node_b\", \"node_c\"]]:\n",
+ "def node_a(state: State) -> Command[Literal[\"node_b\", \"node_c\"]]:\n",
" print(\"Called A\")\n",
" value = random.choice([\"a\", \"b\"])\n",
" # this is a replacement for a conditional edge function\n",
@@ -116,8 +117,8 @@
" else:\n",
" goto = \"node_c\"\n",
"\n",
- " # note how GraphCommand allows you to BOTH update the graph state AND route to the next node\n",
- " return GraphCommand(\n",
+ " # note how Command allows you to BOTH update the graph state AND route to the next node\n",
+ " return Command(\n",
" # this is the state update\n",
" update={\"foo\": value},\n",
" # this is a replacement for an edge\n",
@@ -130,7 +131,6 @@
"\n",
"def node_b(state: State):\n",
" print(\"Called B\")\n",
- " # graph command can also be used\n",
" return {\"foo\": state[\"foo\"] + \"b\"}\n",
"\n",
"\n",
@@ -171,7 +171,7 @@
"source": [
"!!! important\n",
"\n",
- " You might have noticed that we used `GraphCommand` as a return type annotation, e.g. `GraphCommand[Literal[\"node_b\", \"node_c\"]]`. This is necessary for the graph compilation and rendering, and tells LangGraph that `node_a` can navigate to `node_b` and `node_c`."
+ " You might have noticed that we used `Command` as a return type annotation, e.g. `Command[Literal[\"node_b\", \"node_c\"]]`. This is necessary for the graph compilation and rendering, and tells LangGraph that `node_a` can navigate to `node_b` and `node_c`."
]
},
{
@@ -216,13 +216,13 @@
"output_type": "stream",
"text": [
"Called A\n",
- "Called B\n"
+ "Called C\n"
]
},
{
"data": {
"text/plain": [
- "{'foo': 'ab'}"
+ "{'foo': 'bc'}"
]
},
"execution_count": 5,
@@ -237,9 +237,9 @@
],
"metadata": {
"kernelspec": {
- "display_name": "Python 3 (ipykernel)",
+ "display_name": "langgraph",
"language": "python",
- "name": "python3"
+ "name": "langgraph"
},
"language_info": {
"codemirror_mode": {
@@ -251,7 +251,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.12.3"
+ "version": "3.11.9"
}
},
"nbformat": 4,
diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py
index 98f9e350d..28e5a5940 100644
--- a/libs/langgraph/langgraph/types.py
+++ b/libs/langgraph/langgraph/types.py
@@ -252,8 +252,12 @@ class Command(Generic[N]):
- None: the current graph (default)
- GraphCommand.PARENT: closest parent graph
update: state update to apply to the graph's state at the current superstep.
- send: list of `Send` objects to send to other nodes.
resume: value to resume execution with. Will be used when `interrupt()` is called.
+ goto: can be one of the following:
+ - name of the node to navigate to next (any node that belongs to the specified `graph`)
+ - list of node names to navigate to next
+ - `Send` object
+ - sequence of `Send` objects
"""
graph: Optional[str] = None
From 5570121c8388d2d2c32c5f9f8204646c4657cb8c Mon Sep 17 00:00:00 2001
From: vbarda
Date: Wed, 4 Dec 2024 18:56:46 -0500
Subject: [PATCH 136/149] update
---
docs/docs/concepts/low_level.md | 4 ++--
docs/docs/reference/graphs.md | 1 -
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md
index c775eae81..d06dea750 100644
--- a/docs/docs/concepts/low_level.md
+++ b/docs/docs/concepts/low_level.md
@@ -324,7 +324,7 @@ graph.add_conditional_edges("node_a", continue_to_jokes)
## `Command`
-It can be useful to combine control flow (edges) and state updates (nodes). For example, you might want to BOTH perform state updates AND decide which node to go to next in the SAME node. LangGraph provides a way to do so by returning a [`Command`][langgraph.graph.state.GraphCommand] object from node functions:
+It can be useful to combine control flow (edges) and state updates (nodes). For example, you might want to BOTH perform state updates AND decide which node to go to next in the SAME node. LangGraph provides a way to do so by returning a [`Command`][langgraph.types.Command] object from node functions:
```python
def my_node(state: State) -> Command[Literal["my_other_node"]]:
@@ -340,7 +340,7 @@ def my_node(state: State) -> Command[Literal["my_other_node"]]:
| Property | Description |
| --- | --- |
-| `graph` | Graph to send the command to. Supported values:
- `None`: the current graph (default)
- `GraphCommand.PARENT`: closest parent graph |
+| `graph` | Graph to send the command to. Supported values:
- `None`: the current graph (default)
- `Command.PARENT`: closest parent graph |
| `update` | State update to apply to the graph's state at the current superstep |
| `resume` | Value to resume execution with. Will be used when `interrupt()` is called |
| `goto` | Can be one of the following:
- name of the node to navigate to next (any node that belongs to the specified `graph`)
- list of node names to navigate to next
- `Send` object
- sequence of `Send` objects
If `goto` is not specified and there are no other tasks left in the graph, the graph will halt after executing the current superstep. |
diff --git a/docs/docs/reference/graphs.md b/docs/docs/reference/graphs.md
index fef38e159..c67e2136a 100644
--- a/docs/docs/reference/graphs.md
+++ b/docs/docs/reference/graphs.md
@@ -11,7 +11,6 @@
members:
- StateGraph
- CompiledStateGraph
- - GraphCommand
::: langgraph.graph.message
options:
From 19a6e894eb8e39b2a2d085c2c2c7890dbe7f4d06 Mon Sep 17 00:00:00 2001
From: vbarda
Date: Wed, 4 Dec 2024 19:02:49 -0500
Subject: [PATCH 137/149] more updates
---
docs/docs/concepts/low_level.md | 3 +++
docs/docs/how-tos/graph-command.ipynb | 8 ++++----
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md
index d06dea750..3b3806a97 100644
--- a/docs/docs/concepts/low_level.md
+++ b/docs/docs/concepts/low_level.md
@@ -283,6 +283,9 @@ You can optionally provide a dictionary that maps the `routing_function`'s outpu
graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"})
```
+!!! tip
+ Use [`Command`](#command) instead of conditional edges if you need to combine state updates and routing.
+
### Entry Point
The entry point is the first node(s) that are run when the graph starts. You can use the [`add_edge`][langgraph.graph.StateGraph.add_edge] method from the virtual [`START`][langgraph.constants.START] node to the first node to execute to specify where to enter the graph.
diff --git a/docs/docs/how-tos/graph-command.ipynb b/docs/docs/how-tos/graph-command.ipynb
index ad33e3a62..3f3a4c0ef 100644
--- a/docs/docs/how-tos/graph-command.ipynb
+++ b/docs/docs/how-tos/graph-command.ipynb
@@ -83,7 +83,7 @@
"id": "6a08d957-b3d2-4538-bf4a-68ef90a51b98",
"metadata": {},
"source": [
- "## Control flow with Command"
+ "## Define graph"
]
},
{
@@ -237,9 +237,9 @@
],
"metadata": {
"kernelspec": {
- "display_name": "langgraph",
+ "display_name": "Python 3 (ipykernel)",
"language": "python",
- "name": "langgraph"
+ "name": "python3"
},
"language_info": {
"codemirror_mode": {
@@ -251,7 +251,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.9"
+ "version": "3.12.3"
}
},
"nbformat": 4,
From 085395c824b2cdbbe9449a1763e09459f48a771f Mon Sep 17 00:00:00 2001
From: vbarda
Date: Wed, 4 Dec 2024 19:04:42 -0500
Subject: [PATCH 138/149] rename
---
docs/docs/concepts/low_level.md | 2 +-
docs/docs/how-tos/{graph-command.ipynb => command.ipynb} | 0
docs/docs/how-tos/index.md | 2 +-
docs/mkdocs.yml | 2 +-
4 files changed, 3 insertions(+), 3 deletions(-)
rename docs/docs/how-tos/{graph-command.ipynb => command.ipynb} (100%)
diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md
index 3b3806a97..fab9c3e03 100644
--- a/docs/docs/concepts/low_level.md
+++ b/docs/docs/concepts/low_level.md
@@ -380,7 +380,7 @@ def my_node(state: State) -> Command[Literal["my_other_node", "__end__"]]:
return Command(goto="__end__")
```
-Check out this [how-to guide](../how-tos/graph-command.ipynb) for an end-to-end example of how to use `Command`.
+Check out this [how-to guide](../how-tos/command.ipynb) for an end-to-end example of how to use `Command`.
## Persistence
diff --git a/docs/docs/how-tos/graph-command.ipynb b/docs/docs/how-tos/command.ipynb
similarity index 100%
rename from docs/docs/how-tos/graph-command.ipynb
rename to docs/docs/how-tos/command.ipynb
diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md
index 1c60818ae..297bfd3e0 100644
--- a/docs/docs/how-tos/index.md
+++ b/docs/docs/how-tos/index.md
@@ -20,7 +20,7 @@ These how-to guides show how to achieve that controllability.
- [How to create branches for parallel execution](branching.ipynb)
- [How to create map-reduce branches for parallel execution](map-reduce.ipynb)
- [How to control graph recursion limit](recursion-limit.ipynb)
-- [How to combine control flow and state updates with GraphCommand](graph-command.ipynb)
+- [How to combine control flow and state updates with Command](command.ipynb)
### Persistence
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 882e3cdfd..bc1ff59ea 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -151,7 +151,7 @@ nav:
- how-tos/branching.ipynb
- how-tos/map-reduce.ipynb
- how-tos/recursion-limit.ipynb
- - how-tos/graph-command.ipynb
+ - how-tos/command.ipynb
- Persistence:
- Persistence: how-tos#persistence
- how-tos/persistence.ipynb
From f028984b2ee3664869bf16a96796ff1b0ff0b085 Mon Sep 17 00:00:00 2001
From: Vadym Barda
Date: Wed, 4 Dec 2024 19:07:50 -0500
Subject: [PATCH 139/149] langgraph: remove print (#2640)
---
libs/langgraph/langgraph/types.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py
index 67c7e53f8..bd6e94d19 100644
--- a/libs/langgraph/langgraph/types.py
+++ b/libs/langgraph/langgraph/types.py
@@ -350,7 +350,6 @@ def interrupt(value: Any) -> Any:
if tid == NULL_TASK_ID and c == RESUME:
assert len(scratchpad["resume"]) == idx, (scratchpad["resume"], idx)
scratchpad["resume"].append(v)
- print("saving:", scratchpad["resume"])
conf[CONFIG_KEY_SEND]([(RESUME, scratchpad["resume"])])
return v
# no resume value found
From 6caaa8cea7aed7e9cd3e11ff8b22ad6c795a8724 Mon Sep 17 00:00:00 2001
From: Vadym Barda
Date: Wed, 4 Dec 2024 19:16:26 -0500
Subject: [PATCH 140/149] Update libs/langgraph/langgraph/types.py
Co-authored-by: Nuno Campos
---
libs/langgraph/langgraph/types.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py
index 28e5a5940..fcfd99318 100644
--- a/libs/langgraph/langgraph/types.py
+++ b/libs/langgraph/langgraph/types.py
@@ -256,7 +256,7 @@ class Command(Generic[N]):
goto: can be one of the following:
- name of the node to navigate to next (any node that belongs to the specified `graph`)
- list of node names to navigate to next
- - `Send` object
+ - `Send` object (to execute a node with the input provided)
- sequence of `Send` objects
"""
From 1a492f727c12d1c1dfcd5719d82c026e67ca6f2c Mon Sep 17 00:00:00 2001
From: Vadym Barda
Date: Wed, 4 Dec 2024 19:16:33 -0500
Subject: [PATCH 141/149] Update libs/langgraph/langgraph/types.py
Co-authored-by: Nuno Campos
---
libs/langgraph/langgraph/types.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py
index fcfd99318..6503bde3b 100644
--- a/libs/langgraph/langgraph/types.py
+++ b/libs/langgraph/langgraph/types.py
@@ -251,7 +251,7 @@ class Command(Generic[N]):
graph: graph to send the command to. Supported values are:
- None: the current graph (default)
- GraphCommand.PARENT: closest parent graph
- update: state update to apply to the graph's state at the current superstep.
+ update: update to apply to the graph's state.
resume: value to resume execution with. Will be used when `interrupt()` is called.
goto: can be one of the following:
- name of the node to navigate to next (any node that belongs to the specified `graph`)
From 7651f1ab1cfc32ddea62271ccd0922722194b326 Mon Sep 17 00:00:00 2001
From: Vadym Barda
Date: Wed, 4 Dec 2024 19:17:22 -0500
Subject: [PATCH 142/149] Update libs/langgraph/langgraph/types.py
Co-authored-by: Nuno Campos
---
libs/langgraph/langgraph/types.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py
index 6503bde3b..1780c0a7f 100644
--- a/libs/langgraph/langgraph/types.py
+++ b/libs/langgraph/langgraph/types.py
@@ -252,7 +252,7 @@ class Command(Generic[N]):
- None: the current graph (default)
- GraphCommand.PARENT: closest parent graph
update: update to apply to the graph's state.
- resume: value to resume execution with. Will be used when `interrupt()` is called.
+ resume: value to resume execution with. To be used together with `interrupt()`.
goto: can be one of the following:
- name of the node to navigate to next (any node that belongs to the specified `graph`)
- list of node names to navigate to next
From e9cd216887c3fcdfe25ec0090b51bb5c0011a655 Mon Sep 17 00:00:00 2001
From: Vadym Barda
Date: Wed, 4 Dec 2024 19:18:37 -0500
Subject: [PATCH 143/149] Update docs/docs/concepts/low_level.md
Co-authored-by: Nuno Campos
---
docs/docs/concepts/low_level.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md
index fab9c3e03..b3df3c882 100644
--- a/docs/docs/concepts/low_level.md
+++ b/docs/docs/concepts/low_level.md
@@ -284,7 +284,7 @@ graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False:
```
!!! tip
- Use [`Command`](#command) instead of conditional edges if you need to combine state updates and routing.
+ Use [`Command`](#command) instead of conditional edges if you want to combine state updates and routing in a single function.
### Entry Point
From cd875291ad003d74cb0e3f685c6e2ac6227be673 Mon Sep 17 00:00:00 2001
From: William FH <13333726+hinthornw@users.noreply.github.com>
Date: Wed, 4 Dec 2024 16:21:01 -0800
Subject: [PATCH 144/149] Link to conceptual doc (#2641)
---
.../cassettes/semantic-search_10.msgpack.zlib | 1 +
.../cassettes/semantic-search_11.msgpack.zlib | 1 -
.../cassettes/semantic-search_13.msgpack.zlib | 2 +-
.../cassettes/semantic-search_15.msgpack.zlib | 2 +-
.../cassettes/semantic-search_17.msgpack.zlib | 2 +-
.../cassettes/semantic-search_19.msgpack.zlib | 1 +
docs/cassettes/semantic-search_6.msgpack.zlib | 2 +-
docs/cassettes/semantic-search_8.msgpack.zlib | 2 +-
docs/docs/cloud/deployment/semantic_search.md | 4 +-
docs/docs/concepts/memory.md | 3 +
docs/docs/how-tos/index.md | 1 +
.../docs/how-tos/memory/semantic-search.ipynb | 163 +++++++++++++++---
docs/docs/tutorials/tnt-llm/tnt-llm.ipynb | 4 +-
13 files changed, 151 insertions(+), 37 deletions(-)
create mode 100644 docs/cassettes/semantic-search_10.msgpack.zlib
delete mode 100644 docs/cassettes/semantic-search_11.msgpack.zlib
create mode 100644 docs/cassettes/semantic-search_19.msgpack.zlib
diff --git a/docs/cassettes/semantic-search_10.msgpack.zlib b/docs/cassettes/semantic-search_10.msgpack.zlib
new file mode 100644
index 000000000..b55c7fcf2
--- /dev/null
+++ b/docs/cassettes/semantic-search_10.msgpack.zlib
@@ -0,0 +1 @@
+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
\ No newline at end of file
diff --git a/docs/cassettes/semantic-search_11.msgpack.zlib b/docs/cassettes/semantic-search_11.msgpack.zlib
deleted file mode 100644
index efa7dbf7d..000000000
--- a/docs/cassettes/semantic-search_11.msgpack.zlib
+++ /dev/null
@@ -1 +0,0 @@
-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
\ No newline at end of file
diff --git a/docs/cassettes/semantic-search_13.msgpack.zlib b/docs/cassettes/semantic-search_13.msgpack.zlib
index c02706da9..b01db0846 100644
--- a/docs/cassettes/semantic-search_13.msgpack.zlib
+++ b/docs/cassettes/semantic-search_13.msgpack.zlib
@@ -1 +1 @@
-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
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/docs/cassettes/semantic-search_15.msgpack.zlib b/docs/cassettes/semantic-search_15.msgpack.zlib
index c453ed674..f5810c73a 100644
--- a/docs/cassettes/semantic-search_15.msgpack.zlib
+++ b/docs/cassettes/semantic-search_15.msgpack.zlib
@@ -1 +1 @@
-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
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/docs/cassettes/semantic-search_17.msgpack.zlib b/docs/cassettes/semantic-search_17.msgpack.zlib
index f0c9ab212..23469642a 100644
--- a/docs/cassettes/semantic-search_17.msgpack.zlib
+++ b/docs/cassettes/semantic-search_17.msgpack.zlib
@@ -1 +1 @@
-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
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/docs/cassettes/semantic-search_19.msgpack.zlib b/docs/cassettes/semantic-search_19.msgpack.zlib
new file mode 100644
index 000000000..d0604b031
--- /dev/null
+++ b/docs/cassettes/semantic-search_19.msgpack.zlib
@@ -0,0 +1 @@
+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
\ No newline at end of file
diff --git a/docs/cassettes/semantic-search_6.msgpack.zlib b/docs/cassettes/semantic-search_6.msgpack.zlib
index cf7210983..9e8ecf09a 100644
--- a/docs/cassettes/semantic-search_6.msgpack.zlib
+++ b/docs/cassettes/semantic-search_6.msgpack.zlib
@@ -1 +1 @@
-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
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/docs/cassettes/semantic-search_8.msgpack.zlib b/docs/cassettes/semantic-search_8.msgpack.zlib
index 786f820bc..b9b99ebd7 100644
--- a/docs/cassettes/semantic-search_8.msgpack.zlib
+++ b/docs/cassettes/semantic-search_8.msgpack.zlib
@@ -1 +1 @@
-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
\ No newline at end of file
+eNpteHdUU9v2tRQFlCqKBemIFyUQWui9Su+9GEKoIQlJqNJELh1iVLp0JFTpvSpIF0R67yAgIL3DF+/1vd9933v7jzPG3mvuudbea651zjiBeHcoCu2IgBMVOMIxUBQYgiFM0LhAPArq6gZFY4KyXaAYB4Rtlo62vkGmG8pxjNMBg0Gixfn4wEhHXgQSCgc78kIQLnzu/HxQFxuora0j3B6dZYOw9RoDP2d3hCPdMOzirObmQkAeVn6gmCAPqyBIjJ/wBFpa8rCyuyBsoTACgB0D9cQA/s0AEASgXcAwGDsBAoVDEL/WrO0QKBfwLzZ2GzAaChJi98U7QMG2hBNMX6HNckCgMbgP/xlVERgCgSIJvL8pcIX23o5IHlZbqB0MjIHmQRBwOPSvM+PynKFQJAAMc3SHZv+9C1cMRiJhjhDwLzufExoBLyDgMVA4BoDxQkL/25znhoaiAGB7AgJXrk0IQu4pn44X4frgrPy8wiBegWJPABoDdoTDoGg0AAYmxJON/Mte908DEgxxJpAAfqcGl/335g//xCDQuPeaYIi2/n9QglEQB9x7MMoFJFT2z3WUGxzj6ALF4RV0/tvdb+P/uRPk5efnFSj5D2K0FxyCe28HhqGhVf+xGYpBeQEgCAIHLh2YDUEgnB2huLEda2uInbWNi5SRogrYQMPEy9CQ38DLTFfDGuKMVBRw9tITQMORvK5Cru5K2jB1ASN1FyEAv4igoKAwCAQUAPDzAnn5efkBnnZ2MDUvPXUblJKO11M1LUU5RzWzpyZQL0VDI2GIqZ68speeorCgmzyvjokC2o7X1EkFY6DFr+4KRNrD9Z0Vzbz1HFQ9jBVVRdRcRXiB8rxID3UvDwlWQnRu7o62UrqKYqIIFbSWiqs7Ss9EWV4Zo6ahDlUUAasJu6nKeyipCCKQcDgQ6K1u+I/whEAiAODvCEFAIVHgr/HhX9qAQeH2GAdchphIDgqKRhLKCfoym3BjGDd0YBZBhtCeDrwL4e4IKc7QVv8/Bd/NUiRIEtdgDLXlYQUKsSpCIawCBGesAoLiwiBxIUFWFU2DAoXfXgz+pwJLDFBgONqOoEKlfykeD3FwgztDbfMU/qfW635pnZDIX9GjEDDCIgzhAUCgHO0d4bj0xw3/nxnqiUSgoYDfQeMKTAB6fzcKwFPFgr/rDvBXSeMq/3dBl/0GIVD2YLij91+h43J/1QLEgSCq8t9mJArxy++vjS5oXCY/v/CH35Z/yTSPcDdAAD8QAOSvQWNQjhBCVf46PBKBwgDQUAihV2G8cGM8LmDPXyUpJcgvLAgi5EmC1REOgbnZQvXdbBQRLgSfaAlWJAoKQ4Btaz0BKEIOYI4ujoQ8/vX83QcJ5cb/K8vV/43AIJyhhJaZ85edMBr/CUFBf3n4dYx/E2WJEUb9/wb95sKL/TVAtf+JQkP/EU8myAVd/d/23wwZQHSB57/AAEdb3BgnYWINEhAQERW2sRGxgQDFIBBRMBRkJybELyAkICwKBQsLFikoAxTAEAcoQP8vweLwiqZacppPFSpNAP9UHkAb+feLAg9HoOGOdnbZ+lAUITO4PAgM4WZLaK4oaDaBS0/OFFcuCoXY2UD4BaB2wqIQOwE7gL6aQvG/2P6t06xfnRkPhhGS5w7BlTkISrGLCwkJskuwuoClREFCQOBfL5UX2b+SDbf/fBfIEkF+5a9BYpBoTlPaVZm9NVIUn1B1EE6uavhFIu2K2B3ix+7iTLIGsopit3QkTps8fk6MTflC2GNlk6f5p+0H/c/Wi5ou4GV5HSFNl8frW8ZjNP6X2P1xANO3X3P/i1/mk3UZv9RL6+6MzvCJQh8Zvy/+MrFb5vyX533Wk/7Wx01RF4Sdmw2LBgwp+lXfoYbNvanE8zIHgimR5v7XHOHdF5WtTuWTVGOyiuzaTp9NY/g5EpX30rS9FEd0o4MuHP3B+3spHKIygxrFXackXV5mJ7pCsEwlnXlSr5enXEZiHZFKP6RfWPqI0p63oXttRVljW3uGO6cQbs0OriJzE/5QxbjjyiSB7ZK0L2zEC2xdwbBpRCZxxXk/R/DiVjI3490QMybmMD9J7Ng37ZLglHZ7+Q9V+rqaUfv9AVYRTPtqVd2ed6BEQTy3V+VKyOM9scOnp/Rqg9wZvsIeGeLpjOSsYiBjwM3KKOtPaY/Y+bdw5ppMWUSch+zZ0wm+eUsNRWy2zwu5ouUjYAdS71oOT8+zjT47Plv74H0FXTitt1CNvhKSYko5qygzOPiYqk+/UG5vcf7Vcpfcld2LnJgOZEdS/MpUGSOf37Cs7zPqj/XStugXAOZxrdcDnh0W9nemDyVPOJWF/JSS7HYxb5Te2tmyLQd6jGHimtjq5fcVh667xc9NcMjGtWMyRxjAlacaNLd9tRJAWvBHAWI/G0GsKls/+lL5c+6S6FED2gX+qEnhqJRw2LxztrnlzkZatpasVv4W+8qK0Q3Jhbru+d1wLMVprv0HgyaL2DM4gqkhk2OLsrSR5ydD8O4fJtGWX7an89MBVMJSJ+3ObTF86zeaEr4eT3SQN6MWB0np/tAyoIqrWZs+zCZCvyLlNP/AQPt6kStw/MA7tUWuxaz7tsqRqTvN/SdXP9+YwLPTfj55WnQ8ja3eta0sfu5Dh+sW7XNvHg1CXheLYGYZLlvjdGFpSTq6r7xWJT6xOhOmUl9DeWETxJQpGMqwB+xi4j8ISW7pHWuRBR2Gqn6dErBPmImmqXevoeUY9m373j4063j4MItW8gl94uJpwEAgY7gzN/HZnGd5v2GOG0rxHaWdNjFHAONRtrES/K5EdiKPV9dAZtX71G3J8bIbsYsP9Y7X0sJdPnIoqZdxh/nmHh4xEsuEzG9j0mMPkXWWq6JEtLdyufxO7m1/q3SMmKzUUy7prdNCRlEFjckxOD4gJ6K3a3lHex7J9hH1XhGdxn5nZ6YvxvYkF1ouk6pc/sUtTvxzW3dQ/QDxdxZaFQHr2uLOrHfhvCIGr8/UDGS9ekD19Ssii8PLEBCP24h8vARJxXKPfd1uIzVt/MCjEkr1rbse9tJnigCu1HO/vj+G1EnusapcKG7nJ+7W2bAel1Lkek1zDx36SnToSCuN3vnhSvIHgBekitbPVlkBlI1JbjTmeL9+9HFhVBH91cJnlNFBu/zcMjc27hJJ11h5gyKM9cVH0du007GceFM6y7qrZP2B03yN6cydQ+F11nz2Rw6NNP4glSk678IttqLPIuGHpcFiC5PBaeLMnvNaptdijeibgBrbYd+HD2hdXeQVc2JsfuzWF8/O+Qja2N7d/bN6c2F8O27g/Z9VPeI3+6au9xb4yVK3zF/s5HPuM90v6SbOhVuHpi9ba8RMLd/oHeGwaN8a2t7ZfZURMtImZAS4Xfy8qLyYnIVsZr8FUl5nimA5GG7nxOvobuuERIVzcoT6nbQMn3e9PFWp01s4aCjhf/yIZFVz5DSCX74etqsXdJh+Rjswiepnl3n6eOlhb2jFjPVHi5DM67vc/Y46gjEbiwL30qs3oPO4+h92+A3DZoCf8uYborqLP1Ibi8WmrAebXLK+e/gLHWQrbJZ4oOw8Q9nEn7YlJxZtJFOMCt38yci31VMQ7Z9qGKqFL2VucoLY1SXT5oUOLYXMzlnaURJBFavudi3eGvI5sG+6mDYWq6e0596zD/xu/nHx8ZBCleLkgCw22/drwKBuu075HKv1HS5dNvGXB7BNrladzv7nYd2h29h88h4dClWDO9ULtx7UOEBOGNy4VjpHT6VByEbdhJvpiOajRiaDYug29Fa2eet3r3ynHJHK5/NWTzvb1jOvp5dr8uSRem8BLY9zyA1MWP3qu8WDFWgVHt3nytiuoGLI0bo6P0v9ej5fbzh1wC7wapzAvE8gdZF3TzqSmuUixbcXxp7Jm56Gm1nMy+/7gFCPVjjEur7fOeQRbWn5wLg3NPHWFp1D4lcZIitOTsSVvhQzZdBYwOC9fyzb8zUSeHjvCzGFck8/khOQ5LxS88I4vX+1xu/z7Tq8XMSaPS+tVD1g7FY9C01576Xqk7qwWKhk0NeNswAzfJSOZtdTdH+oZgIX/vnaz2HL0YCLwpG9KuvjSWGu71/Ul7baVQLtLwUehiYEatc/cc//LO3jZNJTzjVeUlD38GHnNdREV3rwZ7szKXDpC/bX9nm6RTbWo0FXVW37s0obC+s6a5IoJHP3JOCnb36QqxX6z3FC5BfbYfPJ20hOz8lrXIAPS3f6Shoc022xt3UrVH74KDxb1Hm39RhnrUdiMqjCIcsyp7X3TqsX5mhI+Z5v5XR+NWV+VXSYKiPT8qSSeD46DJurDBT3HswHxQgyHl2da8honvmzCrjAMuAg78cHQeGC0e3JA64v6r8dKzbkWQjjJBned4PFokEYlxC+cDS1SN3Dn1V4zy0gGhkx40kLaYqHBaPYPykQf7hwf5PVq3H9g+z6JQXdrWD7g1KIyaP9BO/JTGL1vH3/PffJWq1EQNy9oW/zLr3SFSnXR3PT+NIu/hBA3W248jNGlZG7th6dylJEFuJcgA8e3n5wDI4sraFFM0djxfFzuyUbbcL+L3u/mhnwt99hlnd9mzbnifDW+VhnoyDB+fXxGw7NPjUqOxJpL/KgQ3nyyQYT6xvtd506je7XaQHknvspZQ7CC6QamE6NK0wo3qRRTk/g1vGcSQqut8A8A6GJVtalvYUoOtjYsfwDc8V4NmOz2ZbptYU3M/78ZTVVNC2hA6p73g35NaEPqVEragf4fPR6dlFJ/ntDbK6Hnh3QdfaTHJEHnz/a+2ukcGdVkA015fyNlNtRPa5pts5s7JvsjWW5Y7Ik0/lEOlhPc7xmVSPF8Lk6om/lvfPwOXlqySmXuGepg3diCqXz0ryejdTtyoHNlzRFpxFyFlqO7IlbB7w804lq2kY9tU9HLTVFs/KUri8ADSiJnIMMmGkuqL+PzkkWjNIaWl827axnQkaeXYeFnXAiM1MbA0WMvOctpjWHaSzrX6ytJOpfZRiB1ffU3a9MHQlOzAuu/fBYgaSC0V7b1l3mRWpqQN3zbnpY2X39Mpz+IihC4zyKW6LMVVyh+hLEXqFqopJcLeks6v86cn5kfUqTpEj1eSlExWkwNtb1k9y+rqQ4h5vP0ytlx/BGncih4PGlgU33pBMz9cPaCtX0LHPJId/U5oSW0IITSi7k7SRrH84RHxnlHmZxRrt9AJdXCblnZexD2Q+Q+jMqi9fRYSLfLu81WZ/NYuWSPgmi+/Z2vjH0IC1TdS/4H2a8vZNvfp2CCxla10vmVCvafWjT9hP+CVf9qMgtdkBkMQ1vwCxlsVSfNORoLj+F2zQI06M7zLeSD2Z+ENWiPUGVPqr66up4DGl7VljwDmftz3LWBGZ2IreIBnp1ZF1TSqvpfp3UDTMalnN4tt1c4u2Wsw1Ka/bK0PPOGFgeSdMVZ2nVBgXvZ+8EN/Q7/aiNneYazCj4imgvC1KH/BXfcFes+9iLmZ4/ay0dx3y0iWnrKNOzL3jaRVabmikhvHbHGKBfWZhuugvazwtV3n1USeWpz9TSZHFgA0rfjVeTHJIRkoaLFpPvc12Vy2zMVbCyrAq9DmTNZ2GZj53suda38SiHlJqV4fty+OOK6Yv5h/wRRrZvpP6UCoybU14azvI8XuLbZpx2yAO5tDo4ds3m7D3GCW/3R33gJitmHktak3tkONCv63w3ykdh/0kV1jsrnXStATaQSUWmsV1IxeqcwYd7CV7lGkwiOhpvi/v+ci9XefUzA0aBsX2ll6k3qNCDJ800McB717k5lQyuPso07jUJBuRcu9zV0Ikn5Tc0Kd8I1m4S/rjK1JDDhZE2b/wz9sq79uuuiB3Qfem1/gBnv/WWJB+sdL51UJLftw4lcZCuvZ1utFOJsfLe4MzYpGkkCeIyGaew8JNa/XGMirmYuc8qRnqe2+spxeSNU5p9kf3yNsjNTX+/j0nhWMGXGzYtC+vyYSW22m1mMjRz9+5LVghPd4puFAz+wUgSY4XnRsc3rTv/3D2NuzM09nbpMdaq/wu+/TDzSxvkp6puaafv2qrLLA/RurKo5lxEx8heWALzn4/NFvo7pgAy41QTc0RbM5ZqTFuBT3p5HtxT/MhEJxHLXDDlUUk6MbWtVvDeYrTAV5/eQizssKn15/atJA+qi+n2pVscZueH967NyLOGTHwVE5IpuIi/JG+V8n0d2p4w1rJCUdHq0xcHL/3Thmi1F7OKtT87jr46hRdVzYCJ3aMPcVel6VQiZWy2HZsKAG4532SFdC9Tdti/3WnmqOU26ucx6bsqNeWiZWFcVkqdK44tb7jIMBGP9xSYMrhWeMU87+mX/JPMOGJ6om2RZRLt96ZROhphshrvmTKKGituHu36L6/sR41q80lN2xu0NMwCET5vcyS1OGpuPXCNnY4RGr1DSt+jrS2pJt8gS5OsMG/dpi6kIPbq6+2uR69nxJeLh7EKreNfpu/AX75s71B1yU0mdQ5KFDcqS8y5Ohgump7V6qScWf5jTi+Wze2hgF3DfDeoelM7+DIcW1tmwF441zY+3tqXfbWvTvXtnHxwNxahGBEZfBfO0YQ/YGb+adHTScySDMLNj/if9XLmpK9EP9hZjjKV2GWrVy7VBEpq9n/zJ75HbkWny3GXBuaEpSYzH3mcW3py6nTx9obnNDXZPOyg/H6FnvmBV7spp3q2MWiu4cknK9bWmUmQ3jC965yO0QzE2qKNJsWOWL+6El81wVTaC+sh3XQP7TfUzlRamZ/v8450fVZ8rNTNwLyUwFgFgYqfK+BEnvl0Sg1Vt4DWUpkM50yPQG/TFsZMzytpQgwqOyybmpLFkkpJpXF1r27R+Lahqm+wWbYFmlXVtp539c12yljlBvLQXIaxTHTfiT7NEPyJ2hGlM4pfvTiq7nxmPKNHfoMXLHRKGi34PG6B3PxAtWPMRsW+88Dc0GwXgfJHMYKsH2gFU1FwZAiNiEOu9hvGf8Zj+aZT+Ok3++pDnvDdD/pE3F+6xj1qXJXfNLN1GDBeaSN/jGldbDxItV4h257ZVCth/d6bg4gh3U65jGi/Pnr1sCMopFikE7rky3NzcYvlQgQRyMEAz6b42FhibtNYaQ5shLFdk5HJlBZOLIEA4LIX5PqpSsWkUlv918yY/TOUXcWN3uRS3o8na8wkW3WwlXcJesADN1L33SiH5KdqPnN4UdTXMHMKq5GcPInb0l06B2Yu7Uv/+bMqldrZ37ih8uNRJ9s+xatkMYdN6fpZEzbjk+Be4J90iqIF7HmNkXwpT308Zb8t06CqJzXsuSfZQmT8b7k85l8/VuPWOUxBeSukliY638v9KjxO/2WgjCjoFsM+diWqGUL3Xc8TnjXUpKek3zvEd9GyV2nccV+medOSk1+yN1wT0liZ8Mq6jFYAX0+HWE/5yHJJ+npxb5ckYKA8jeizF/qe/3Hds5i4m+G7mu+P2Db5B6yjr/PNxUQNKrBNF9arTWbbh+Sc0Rl3b8ZWIAXbBaT2Nbakn2CDsh/Qej9MZ6C42g+t3lF7vaUpkeh/0rgjgYptArdZN56S5byvU3Nh6ocNvRT0eQJWqZDA2yiPWV0UPrpMD8M6yTtQ444p9PIkHuZ8L7uNGgQfHqtYe/NNVN+ktboh1bOcfnK2Z1JYxcsm8Rqqf+4E+f5t66Xn6ofe5WxljllGmyiH/TfvPO8QX9FOevfqZaKSz4cJjmdvinR98i1AbiGiiidsjwYvqQ1FNBl9lOf6J+lSG0Qid7YGroWfkN3MrKke26n+QUVxtS7eLCLdvYdm45oN70T0d1ypk7j2zp3GJ+vy9d/c2N2nx+8b4LeSEivZlMsVXKjuCPvSn8IXoi15A2lomMeOTsUYx9aK1G9ZSHcgajjyqrnP380BpjtaP6wSxwOu3ReQDMM7n5HyzN0lW3EG9reuGwIz3sDkl4acMF/8k5dqWz9qW4e6ltNL5kb0ZM4m3fBbqO8Rtn5Hv7ocJWEjCB2ayfg2K3h8XXqwhsuGb4vnvLNImAzxbg5TqdBRjlmo2BpgS+x9c/zn2N08RbdSxD6jzDO3OY2zrYA6r4Hkxp/teLA3aNRn+pAtrZHnaGFNJTseYfvHu4jTNT53mNgDLJdPUu6Gy1dm9wGpSOLRUgE26doaXf+macgz7WvML6wfiAffFr5Gh+Ed7BmgiKTzTEC6RpPZWHLdBPjxIZ7dT5HPQ1ORSt2TomPeyOZs2yu5PIIeFft80Ta+EG+P0PtkHhWnG/9F4VGa9leOeonnrXxL9NO+BUncDBkBuVas/eCqdeZPQwi9HdFxy5BJqPyKOoXfxRWaOd/l/ifxVULCZpEr4ICHPYcfIxx8J+e3oQjIUQrLdTFhQ1PNo72Y2a6XbVL3auC47PbQ74yQx5n6MYCRhzSzE0vfKvyFzLLuxDcvI0n3UOylu+9NAGzCsvIuktXrJ/pU5TvUN0kVTL7Ujehf+15WR9/dUPkEa2Nf3c9ys0S4sXvUaEM94KRdNbl7ItTogrhfTEB83mdM6kT0m2bwTf4xE7IEC4qu6sGmYxbwVa2AsQRb4nwNrKKlF+T83tIDOEfVo9jXN6gHvsQRjZwFpcRpgHeFNh+aNB2QrVmVUmiPTruk6eZonyywqLs+1c8Z0VMJayv5oS21XCX9UXLH82J7OhMc4qcAjgUslGt0RL3aXjj1nMfKvSgvaTbInfEVebtAK2ueUSGTMsPj1GJ+N00oB5tV7fLNwViChfxb8oZeVFHTbN/uUDIX7elU9NDR9CEtjGYIl7gz0CWaYo5K28wnGepZjyiRMDPLOTBTeepFU7fZzpJqrptuGunVXOkhERJuY/LkssmNUvOYGFvQFL2U1zT4oPzBxZqEU1cNHZ8HMLiux8UHvmRvkcG1mroPGZp92b7n6NKsbS304O6z/RD+AKDaWWE3Nr5wX5JVDY8qkJ/4eGZ8D23SUJtwMUwjYdJLGpJRrae0vhHJHEgSYtS01+3fbMbnNIedEccquRO+rr68uDcfcL8r96P622eMYiqq54fytU777Ssy+0Q/XokmSZ3zfzqQbYnBbtUtUSxf3Yhc1ZgaS5va8RMMHT6LSXzCd7at91267NtlrVU8RT5L4AMzWLhCHdoOsw9CdrXRbOdRuR8poHbShzXf8UnpZy50bDccH6uHG09Lt3s6zjZ0wlgCNIlj85eT57oYfYt33s+9YHkoGZS5OtJO1e4dvsZXYJWaQLt+TDowvcX23Vcq3bNVfeSxzcKWoabnSaoYI5E6/QflF6YSezB1iZCEJEsi40tRqwPN0f7MhE5kXZEZixueb/NzHW1gq8dnax+iiLpI/sWK3Q0aliVaNOSnry1i3ull4kag5x7bwzLVyaI3xTKn9Io6+PBdv+IlYxnTBTxWbaQzSC2E60HQvNKPaoX+Ij9yJysr1W9Hool9Iv6F2AYy+cd1nW33ZCz/yLq5b67N5inyIOI+l9b+vbiK6dGZaAuq0qOd9GpBQZCoC5+CDvenI7Vgy/LFH6MZm7v1dFKs7iA2l7hm43HKweiwlUjgxaLO6PLMds0wh7838WuRfuWvt3Eti3ljzSwshzucRC2F2z+sG5xhM3gPkBtq9lMJh8PZ2Zq6c73ijxp17th8+0HsWaZysAXj4ISGprDlgtFLBq/ksJZnLxJq5MQszgtpRk5gTVswupPqvPYN8aZMm97HtVY7b1uHF0juZrfrnPxQU0tukfsQj+Hta96oaqcriWir1gm2/xozEiVD1ic54RXeLKcSKZ7x8Dn++eXa1ak5CX1TLMnXiz37QhG9hrAtz83EdVIZL1Pc/gWTSIINj6/dglYnFw5I36d146azY9h54V5oGQc88IUv6ilxEEm8RxGW8vIi+fpRn//5/sjlSTLQYdx824zmGuVnybPTJuz1M/Mjj5msopFUCDkkVTq86fK4iJ7c37+28pjy8rnD+vzUr1/x86mZ1/xpNl41XqR2X5qx7KVenshcuXLl8pLkCtfBFHCa9cqV/weuMSHv
\ No newline at end of file
diff --git a/docs/docs/cloud/deployment/semantic_search.md b/docs/docs/cloud/deployment/semantic_search.md
index 1ea48be13..c9dc58633 100644
--- a/docs/docs/cloud/deployment/semantic_search.md
+++ b/docs/docs/cloud/deployment/semantic_search.md
@@ -111,8 +111,8 @@ from langgraph_sdk import get_client
async def search_store():
client = get_client()
- results = await client.store.search(
- namespace=("memory", "facts"),
+ results = await client.store.search_items(
+ ("memory", "facts"),
query="your search query",
limit=3 # number of results to return
)
diff --git a/docs/docs/concepts/memory.md b/docs/docs/concepts/memory.md
index 126b35993..cdcd8ae5b 100644
--- a/docs/docs/concepts/memory.md
+++ b/docs/docs/concepts/memory.md
@@ -236,6 +236,9 @@ Different applications require various types of memory. Although the analogy isn
[Semantic memory](https://en.wikipedia.org/wiki/Semantic_memory), both in humans and AI agents, involves the retention of specific facts and concepts. In humans, it can include information learned in school and the understanding of concepts and their relationships. For AI agents, semantic memory is often used to personalize applications by remembering facts or concepts from past interactions.
+> Note: Not to be confused with "semantic search" which is a technique for finding similar content using "meaning" (usually as embeddings). Semantic memory is a term from psychology, referring to storing facts and knowledge, while semantic search is a method for retrieving information based on meaning rather than exact matches.
+
+
#### Profile
Semantic memories can be managed in different ways. For example, memories can be a single, continuously updated "profile" of well-scoped and specific information about a user, organization, or other entity (including the agent itself). A profile is generally just a JSON document with various key-value pairs you've selected to represent your domain.
diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md
index 5efbe1969..f0c2f841d 100644
--- a/docs/docs/how-tos/index.md
+++ b/docs/docs/how-tos/index.md
@@ -120,6 +120,7 @@ These guides show how to use the prebuilt ReAct agent:
- [How to add a custom system prompt to a ReAct agent](create-react-agent-system-prompt.ipynb)
- [How to add human-in-the-loop processes to a ReAct agent](create-react-agent-hitl.ipynb)
- [How to create prebuilt ReAct agent from scratch](react-agent-from-scratch.ipynb)
+- [How to add semantic search for long-term memory to a ReAct agent](memory/semantic-search.ipynb#using-in-create-react-agent)
## LangGraph Platform
diff --git a/docs/docs/how-tos/memory/semantic-search.ipynb b/docs/docs/how-tos/memory/semantic-search.ipynb
index 658e4bb29..8e7a8d057 100644
--- a/docs/docs/how-tos/memory/semantic-search.ipynb
+++ b/docs/docs/how-tos/memory/semantic-search.ipynb
@@ -8,12 +8,15 @@
"\n",
"This guide shows how to enable semantic search in your agent's memory store. This lets search for items in the store by semantic similarity.\n",
"\n",
+ "!!! tip Prerequisites\n",
+ " This guide assumes familiarity with the [memory in LangGraph](https://langchain-ai.github.io/langgraph/concepts/memory/).\n",
+ "\n",
"First, install this guide's prerequisites."
]
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": null,
"metadata": {},
"outputs": [],
"source": [
@@ -23,7 +26,7 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
@@ -48,9 +51,18 @@
},
{
"cell_type": "code",
- "execution_count": 25,
+ "execution_count": 2,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/var/folders/gf/6rnp_mbx5914kx7qmmh7xzmw0000gn/T/ipykernel_83572/2318027494.py:5: LangChainBetaWarning: The function `init_embeddings` is in beta. It is actively being worked on, so the API may change.\n",
+ " embeddings = init_embeddings(\"openai:text-embedding-3-small\")\n"
+ ]
+ }
+ ],
"source": [
"from langchain.embeddings import init_embeddings\n",
"from langgraph.store.memory import InMemoryStore\n",
@@ -74,7 +86,7 @@
},
{
"cell_type": "code",
- "execution_count": 26,
+ "execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
@@ -95,7 +107,7 @@
},
{
"cell_type": "code",
- "execution_count": 27,
+ "execution_count": 4,
"metadata": {},
"outputs": [
{
@@ -122,12 +134,73 @@
"source": [
"## Using in your agent\n",
"\n",
- "Add semantic search to any node by injecting the store:"
+ "Add semantic search to any node by injecting the store."
]
},
{
"cell_type": "code",
- "execution_count": 40,
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "What are you in the mood for? Since you love Italian food and pizza, would you like to order a pizza or try making one at home?"
+ ]
+ }
+ ],
+ "source": [
+ "from typing import Optional\n",
+ "\n",
+ "from langchain.chat_models import init_chat_model\n",
+ "from langgraph.store.base import BaseStore\n",
+ "\n",
+ "from langgraph.graph import START, MessagesState, StateGraph\n",
+ "\n",
+ "llm = init_chat_model(\"openai:gpt-4o-mini\")\n",
+ "\n",
+ "\n",
+ "def chat(state, *, store: BaseStore):\n",
+ " # Search based on user's last message\n",
+ " items = store.search(\n",
+ " (\"user_123\", \"memories\"), query=state[\"messages\"][-1].content, limit=2\n",
+ " )\n",
+ " memories = \"\\n\".join(item.value[\"text\"] for item in items)\n",
+ " memories = f\"## Memories of user\\n{memories}\" if memories else \"\"\n",
+ " response = llm.invoke(\n",
+ " [\n",
+ " {\"role\": \"system\", \"content\": f\"You are a helpful assistant.\\n{memories}\"},\n",
+ " *state[\"messages\"],\n",
+ " ]\n",
+ " )\n",
+ " return {\"messages\": [response]}\n",
+ "\n",
+ "\n",
+ "builder = StateGraph(MessagesState)\n",
+ "builder.add_node(chat)\n",
+ "builder.add_edge(START, \"chat\")\n",
+ "graph = builder.compile(store=store)\n",
+ "\n",
+ "for message, metadata in graph.stream(\n",
+ " input={\"messages\": [{\"role\": \"user\", \"content\": \"I'm hungry\"}]},\n",
+ " stream_mode=\"messages\",\n",
+ "):\n",
+ " print(message.content, end=\"\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Using in `create_react_agent`\n",
+ "\n",
+ "Add semantic search to your tool calling agent by injecting the store in the `state_modifier`. You can also use the store in a tool to let your agent manually store or search for memories."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
@@ -142,7 +215,7 @@
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"\n",
- "def add_memories(state, *, store: BaseStore):\n",
+ "def prepare_messages(state, *, store: BaseStore):\n",
" # Search based on user's last message\n",
" items = store.search(\n",
" (\"user_123\", \"memories\"), query=state[\"messages\"][-1].content, limit=2\n",
@@ -154,6 +227,7 @@
" ] + state[\"messages\"]\n",
"\n",
"\n",
+ "# You can also use the store directly within a tool!\n",
"def upsert_memory(\n",
" content: str,\n",
" *,\n",
@@ -161,6 +235,7 @@
" store: Annotated[BaseStore, InjectedToolArg],\n",
"):\n",
" \"\"\"Upsert a memory in the database.\"\"\"\n",
+ " # The LLM can use this tool to store a new memory\n",
" mem_id = memory_id or uuid.uuid4()\n",
" store.put(\n",
" (\"user_123\", \"memories\"),\n",
@@ -173,26 +248,28 @@
"agent = create_react_agent(\n",
" init_chat_model(\"openai:gpt-4o-mini\"),\n",
" tools=[upsert_memory],\n",
- " state_modifier=add_memories,\n",
+ " # The state_modifier is run to prepare the messages for the LLM. It is called\n",
+ " # right before each LLM call\n",
+ " state_modifier=prepare_messages,\n",
" store=store,\n",
")"
]
},
{
"cell_type": "code",
- "execution_count": 44,
+ "execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "What are you in the mood for? Since you love Italian food and pizza, would you like some recommendations for a delicious pizza or a different Italian dish?"
+ "What are you in the mood for? Since you love Italian food and pizza, maybe something in that realm would be great! Would you like suggestions for a specific dish or restaurant?"
]
}
],
"source": [
- "async for message, metadata in agent.astream(\n",
+ "for message, metadata in agent.stream(\n",
" input={\"messages\": [{\"role\": \"user\", \"content\": \"I'm hungry\"}]},\n",
" stream_mode=\"messages\",\n",
"):\n",
@@ -212,9 +289,31 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 8,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Expect mem 2\n",
+ "Item: mem2; Score (0.5895009051396596)\n",
+ "Memory: Ate alone at home\n",
+ "Emotion: felt a bit lonely\n",
+ "\n",
+ "Expect mem1\n",
+ "Item: mem1; Score (0.6207546534134083)\n",
+ "Memory: Had pizza with friends at Mario's\n",
+ "Emotion: felt happy and connected\n",
+ "\n",
+ "Expect random lower score (ravioli not indexed)\n",
+ "Item: mem1; Score (0.2686278787315685)\n",
+ "Memory: Had pizza with friends at Mario's\n",
+ "Emotion: felt happy and connected\n",
+ "\n"
+ ]
+ }
+ ],
"source": [
"# Configure store to embed both memory content and emotional context\n",
"store = InMemoryStore(\n",
@@ -276,7 +375,7 @@
},
{
"cell_type": "code",
- "execution_count": 57,
+ "execution_count": 9,
"metadata": {},
"outputs": [
{
@@ -284,12 +383,12 @@
"output_type": "stream",
"text": [
"Expect mem1\n",
- "Item: mem1; Score (0.3374698138722726)\n",
+ "Item: mem1; Score (0.3374968677940555)\n",
"Memory: I love spicy food\n",
"Context: At a Thai restaurant\n",
"\n",
"Expect mem2\n",
- "Item: mem2; Score (0.3679447999059255)\n",
+ "Item: mem2; Score (0.36784461593247436)\n",
"Memory: The restaurant was too loud\n",
"Context: Dinner at an Italian place\n",
"\n"
@@ -353,9 +452,26 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 10,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Expect mem1\n",
+ "Item: mem1; Score (0.32269984224327286)\n",
+ "Memory: I love chocolate ice cream\n",
+ "Type: preference\n",
+ "\n",
+ "Expect low score (mem2 not indexed)\n",
+ "Item: mem1; Score (0.010241633698527089)\n",
+ "Memory: I love chocolate ice cream\n",
+ "Type: preference\n",
+ "\n"
+ ]
+ }
+ ],
"source": [
"store = InMemoryStore(index={\"embed\": embeddings, \"dims\": 1536, \"fields\": [\"memory\"]})\n",
"\n",
@@ -390,13 +506,6 @@
" print(f\"Memory: {r.value['memory']}\")\n",
" print(f\"Type: {r.value['type']}\\n\")"
]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": []
}
],
"metadata": {
diff --git a/docs/docs/tutorials/tnt-llm/tnt-llm.ipynb b/docs/docs/tutorials/tnt-llm/tnt-llm.ipynb
index c68fb81e1..d952ab592 100644
--- a/docs/docs/tutorials/tnt-llm/tnt-llm.ipynb
+++ b/docs/docs/tutorials/tnt-llm/tnt-llm.ipynb
@@ -43,7 +43,7 @@
"outputs": [],
"source": [
"%%capture --no-stderr\n",
- "%pip install -U langgraph langchain_anthropic langsmith\n",
+ "%pip install -U langgraph langchain_anthropic langsmith langchain-community\n",
"%pip install -U sklearn langchain_openai"
]
},
@@ -632,7 +632,7 @@
"metadata": {},
"outputs": [],
"source": [
- "from langchain.cache import InMemoryCache\n",
+ "from langchain_community.cache import InMemoryCache\n",
"from langchain.globals import set_llm_cache\n",
"\n",
"# Optional. If you are running into errors or rate limits and want to avoid repeated computation,\n",
From 1eeb90ae0d77bbe280df85c620dbbccd16323367 Mon Sep 17 00:00:00 2001
From: vbarda
Date: Wed, 4 Dec 2024 19:31:46 -0500
Subject: [PATCH 145/149] cr
---
docs/docs/concepts/low_level.md | 14 ++++++++------
docs/docs/reference/types.md | 1 +
libs/langgraph/langgraph/types.py | 6 ++++--
3 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md
index b3df3c882..1d059568b 100644
--- a/docs/docs/concepts/low_level.md
+++ b/docs/docs/concepts/low_level.md
@@ -344,9 +344,9 @@ def my_node(state: State) -> Command[Literal["my_other_node"]]:
| Property | Description |
| --- | --- |
| `graph` | Graph to send the command to. Supported values:
- `None`: the current graph (default)
- `Command.PARENT`: closest parent graph |
-| `update` | State update to apply to the graph's state at the current superstep |
-| `resume` | Value to resume execution with. Will be used when `interrupt()` is called |
-| `goto` | Can be one of the following:
- name of the node to navigate to next (any node that belongs to the specified `graph`)
- list of node names to navigate to next
- `Send` object
- sequence of `Send` objects
If `goto` is not specified and there are no other tasks left in the graph, the graph will halt after executing the current superstep. |
+| `update` | Update to apply to the graph's state. |
+| `resume` | Value to resume execution with. To be used together with [`interrupt()`][langgraph.types.interrupt]. |
+| `goto` | Can be one of the following:
- name of the node to navigate to next (any node that belongs to the specified `graph`)
- sequence of node names to navigate to next
- `Send` object (to execute a node with the input provided)
- sequence of `Send` objects
If `goto` is not specified and there are no other tasks left in the graph, the graph will halt after executing the current superstep. |
```python
from langgraph.graph import StateGraph, START
@@ -373,13 +373,15 @@ graph = builder.compile()
With `Command` you can also achieve dynamic control flow behavior (identical to [conditional edges](#conditional-edges)):
```python
-def my_node(state: State) -> Command[Literal["my_other_node", "__end__"]]:
+def my_node(state: State) -> Command[Literal["my_other_node"]]:
if state["foo"] == "bar":
return Command(update={"foo": "baz"}, goto="my_other_node")
- else:
- return Command(goto="__end__")
```
+!!! important
+
+ When returning `Command` in your node functions, you must add return type annotations with the list of node names the node is routing to, e.g. `Command[Literal["node_b", "node_c"]]`. This is necessary for the graph compilation and rendering, and tells LangGraph that `node_a` can navigate to `node_b` and `node_c`.
+
Check out this [how-to guide](../how-tos/command.ipynb) for an end-to-end example of how to use `Command`.
## Persistence
diff --git a/docs/docs/reference/types.md b/docs/docs/reference/types.md
index 98b1ef137..b42b11f35 100644
--- a/docs/docs/reference/types.md
+++ b/docs/docs/reference/types.md
@@ -14,3 +14,4 @@
- StateSnapshot
- Send
- Command
+ - interrupt
diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py
index 1780c0a7f..d4fb5ab55 100644
--- a/libs/langgraph/langgraph/types.py
+++ b/libs/langgraph/langgraph/types.py
@@ -249,13 +249,15 @@ class Command(Generic[N]):
Args:
graph: graph to send the command to. Supported values are:
+
- None: the current graph (default)
- GraphCommand.PARENT: closest parent graph
update: update to apply to the graph's state.
- resume: value to resume execution with. To be used together with `interrupt()`.
+ resume: value to resume execution with. To be used together with [`interrupt()`][langgraph.types.interrupt].
goto: can be one of the following:
+
- name of the node to navigate to next (any node that belongs to the specified `graph`)
- - list of node names to navigate to next
+ - sequence of node names to navigate to next
- `Send` object (to execute a node with the input provided)
- sequence of `Send` objects
"""
From f40a2d71ec4fbd6b4d8cf37eab75b3f91001e751 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 4 Dec 2024 17:15:17 -0800
Subject: [PATCH 146/149] lib 0.2.56
---
libs/langgraph/pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml
index cee26320e..0a1f0b084 100644
--- a/libs/langgraph/pyproject.toml
+++ b/libs/langgraph/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
-version = "0.2.55"
+version = "0.2.56"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
From d1aaa9de8ca6d1079ee245185092661f33194fc6 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 4 Dec 2024 17:25:51 -0800
Subject: [PATCH 147/149] sdk-py: Handle stream(params=)
---
libs/sdk-py/langgraph_sdk/client.py | 22 ++++++++++++++++++----
1 file changed, 18 insertions(+), 4 deletions(-)
diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py
index c453eb749..3436188c7 100644
--- a/libs/sdk-py/langgraph_sdk/client.py
+++ b/libs/sdk-py/langgraph_sdk/client.py
@@ -278,7 +278,12 @@ class HttpClient:
raise e
async def stream(
- self, path: str, method: str, *, json: Optional[dict] = None
+ self,
+ path: str,
+ method: str,
+ *,
+ json: Optional[dict] = None,
+ params: Optional[QueryParamTypes] = None,
) -> AsyncIterator[StreamPart]:
"""Stream results using SSE."""
headers, content = await aencode_json(json)
@@ -286,7 +291,7 @@ class HttpClient:
headers["Cache-Control"] = "no-store"
async with self.client.stream(
- method, path, headers=headers, content=content
+ method, path, headers=headers, content=content, params=params
) as res:
# check status
try:
@@ -314,6 +319,8 @@ class HttpClient:
async def aencode_json(json: Any) -> tuple[dict[str, str], bytes]:
+ if json is None:
+ return {}, None
body = await asyncio.get_running_loop().run_in_executor(
None,
orjson.dumps,
@@ -2447,11 +2454,18 @@ class SyncHttpClient:
raise e
def stream(
- self, path: str, method: str, *, json: Optional[dict] = None
+ self,
+ path: str,
+ method: str,
+ *,
+ json: Optional[dict] = None,
+ params: Optional[QueryParamTypes] = None,
) -> Iterator[StreamPart]:
"""Stream the results of a request using SSE."""
headers, content = encode_json(json)
- with self.client.stream(method, path, headers=headers, content=content) as res:
+ with self.client.stream(
+ method, path, headers=headers, content=content, params=params
+ ) as res:
# check status
try:
res.raise_for_status()
From 63ea71548bf4a5c8006d738f109109f49104f087 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 4 Dec 2024 17:27:24 -0800
Subject: [PATCH 148/149] sdk-py 0.1.43
---
libs/sdk-py/pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml
index edf8a2510..991076ea9 100644
--- a/libs/sdk-py/pyproject.toml
+++ b/libs/sdk-py/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
-version = "0.1.42"
+version = "0.1.43"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"
From a54587cff55d49a30ade11fa3f5448a2a01ac600 Mon Sep 17 00:00:00 2001
From: Nuno Campos
Date: Wed, 4 Dec 2024 17:33:54 -0800
Subject: [PATCH 149/149] Remove unknown arg
---
.github/workflows/_lint.yml | 1 -
.github/workflows/_test.yml | 1 -
.github/workflows/_test_release.yml | 1 -
.github/workflows/release.yml | 4 ----
4 files changed, 7 deletions(-)
diff --git a/.github/workflows/_lint.yml b/.github/workflows/_lint.yml
index c6616bb4b..69b990b6a 100644
--- a/.github/workflows/_lint.yml
+++ b/.github/workflows/_lint.yml
@@ -42,7 +42,6 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
poetry-version: ${{ env.POETRY_VERSION }}
- working-directory: ${{ inputs.working-directory }}
cache-key: lint-${{ inputs.working-directory }}
- name: Check Poetry File
diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml
index 29eab4cd3..3329da546 100644
--- a/.github/workflows/_test.yml
+++ b/.github/workflows/_test.yml
@@ -31,7 +31,6 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
poetry-version: ${{ env.POETRY_VERSION }}
- working-directory: ${{ inputs.working-directory }}
cache-key: test-${{ inputs.working-directory }}
- name: Login to Docker Hub
uses: docker/login-action@v3
diff --git a/.github/workflows/_test_release.yml b/.github/workflows/_test_release.yml
index a4d81e1e2..46e065d33 100644
--- a/.github/workflows/_test_release.yml
+++ b/.github/workflows/_test_release.yml
@@ -29,7 +29,6 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
poetry-version: ${{ env.POETRY_VERSION }}
- working-directory: ${{ inputs.working-directory }}
cache-key: release
# We want to keep this build stage *separate* from the release stage,
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d3d8626aa..d1d5b2aaf 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -31,7 +31,6 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
poetry-version: ${{ env.POETRY_VERSION }}
- working-directory: ${{ inputs.working-directory }}
cache-key: release
# We want to keep this build stage *separate* from the release stage,
@@ -169,7 +168,6 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
poetry-version: ${{ env.POETRY_VERSION }}
- working-directory: ${{ inputs.working-directory }}
- name: Import published package
shell: bash
@@ -256,7 +254,6 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
poetry-version: ${{ env.POETRY_VERSION }}
- working-directory: ${{ inputs.working-directory }}
cache-key: release
- uses: actions/download-artifact@v4
@@ -298,7 +295,6 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
poetry-version: ${{ env.POETRY_VERSION }}
- working-directory: ${{ inputs.working-directory }}
cache-key: release
- uses: actions/download-artifact@v4