\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": "9ac1c2cd-81fb-40eb-8ba1-e9197800cba6",
+ "metadata": {},
+ "source": [
+ "## Create Index"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "b224e5ba-50ca-495a-a7fa-0f75a080e03c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "### Build Index\n",
+ "\n",
+ "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
+ "from langchain_community.document_loaders import WebBaseLoader\n",
+ "from langchain_community.vectorstores import Chroma\n",
+ "from langchain_openai import OpenAIEmbeddings\n",
+ "\n",
+ "### from langchain_cohere import CohereEmbeddings\n",
+ "\n",
+ "# Set embeddings\n",
+ "embd = OpenAIEmbeddings()\n",
+ "\n",
+ "# Docs to index\n",
+ "urls = [\n",
+ " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
+ "]\n",
+ "\n",
+ "# Load\n",
+ "docs = [WebBaseLoader(url).load() for url in urls]\n",
+ "docs_list = [item for sublist in docs for item in sublist]\n",
+ "\n",
+ "# Split\n",
+ "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
+ " chunk_size=500, chunk_overlap=0\n",
+ ")\n",
+ "doc_splits = text_splitter.split_documents(docs_list)\n",
+ "\n",
+ "# Add to vectorstore\n",
+ "vectorstore = Chroma.from_documents(\n",
+ " documents=doc_splits,\n",
+ " collection_name=\"rag-chroma\",\n",
+ " embedding=embd,\n",
+ ")\n",
+ "retriever = vectorstore.as_retriever()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0f52b427-750c-40f8-8893-e9caab3afd8d",
+ "metadata": {},
+ "source": [
+ "## LLMs"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "4dec9d98-f3dc-4b7f-abc0-9d01c754f2be",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "datasource='web_search'\n",
+ "datasource='vectorstore'\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Router\n",
+ "\n",
+ "from typing import Literal\n",
+ "\n",
+ "from langchain_core.prompts import ChatPromptTemplate\n",
+ "from langchain_core.pydantic_v1 import BaseModel, Field\n",
+ "from langchain_openai import ChatOpenAI\n",
+ "\n",
+ "\n",
+ "# Data model\n",
+ "class RouteQuery(BaseModel):\n",
+ " \"\"\"Route a user query to the most relevant datasource.\"\"\"\n",
+ "\n",
+ " datasource: Literal[\"vectorstore\", \"web_search\"] = Field(\n",
+ " ...,\n",
+ " description=\"Given a user question choose to route it to web search or a vectorstore.\",\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# LLM with function call\n",
+ "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
+ "structured_llm_router = llm.with_structured_output(RouteQuery)\n",
+ "\n",
+ "# Prompt\n",
+ "system = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\n",
+ "The vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\n",
+ "Use the vectorstore for questions on these topics. Otherwise, use web-search.\"\"\"\n",
+ "route_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"system\", system),\n",
+ " (\"human\", \"{question}\"),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "question_router = route_prompt | structured_llm_router\n",
+ "print(\n",
+ " question_router.invoke(\n",
+ " {\"question\": \"Who will the Bears draft first in the NFL draft?\"}\n",
+ " )\n",
+ ")\n",
+ "print(question_router.invoke({\"question\": \"What are the types of agent memory?\"}))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "856801cb-f42a-44e7-956f-47845e3664ca",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "binary_score='no'\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Retrieval Grader\n",
+ "\n",
+ "\n",
+ "# Data model\n",
+ "class GradeDocuments(BaseModel):\n",
+ " \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n",
+ "\n",
+ " binary_score: str = Field(\n",
+ " description=\"Documents are relevant to the question, 'yes' or 'no'\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# LLM with function call\n",
+ "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
+ "structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
+ "\n",
+ "# Prompt\n",
+ "system = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
+ " If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n",
+ " It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n",
+ " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n",
+ "grade_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"system\", system),\n",
+ " (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "retrieval_grader = grade_prompt | structured_llm_grader\n",
+ "question = \"agent memory\"\n",
+ "docs = retriever.get_relevant_documents(question)\n",
+ "doc_txt = docs[1].page_content\n",
+ "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "2272333e-50b2-42ab-b472-e1055a3b94a8",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The design of generative agents combines LLM with memory, planning, and reflection mechanisms to enable agents to behave based on past experience and interact with other agents. Memory stream is a long-term memory module that records agents' experiences in natural language. The retrieval model surfaces context to inform the agent's behavior based on relevance, recency, and importance.\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Generate\n",
+ "\n",
+ "from langchain import hub\n",
+ "from langchain_core.output_parsers import StrOutputParser\n",
+ "\n",
+ "# Prompt\n",
+ "prompt = hub.pull(\"rlm/rag-prompt\")\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n",
+ "\n",
+ "\n",
+ "# Post-processing\n",
+ "def format_docs(docs):\n",
+ " return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
+ "\n",
+ "\n",
+ "# Chain\n",
+ "rag_chain = prompt | llm | StrOutputParser()\n",
+ "\n",
+ "# Run\n",
+ "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
+ "print(generation)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "f0c08d14-77a0-4eed-b882-2d636abb22a3",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "GradeHallucinations(binary_score='yes')"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Hallucination Grader\n",
+ "\n",
+ "\n",
+ "# Data model\n",
+ "class GradeHallucinations(BaseModel):\n",
+ " \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n",
+ "\n",
+ " binary_score: str = Field(\n",
+ " description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# LLM with function call\n",
+ "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
+ "structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n",
+ "\n",
+ "# Prompt\n",
+ "system = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n",
+ " Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\n",
+ "hallucination_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"system\", system),\n",
+ " (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "hallucination_grader = hallucination_prompt | structured_llm_grader\n",
+ "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "ded99680-437a-4c9d-b860-619c88949d84",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "GradeAnswer(binary_score='yes')"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Answer Grader\n",
+ "\n",
+ "\n",
+ "# Data model\n",
+ "class GradeAnswer(BaseModel):\n",
+ " \"\"\"Binary score to assess answer addresses question.\"\"\"\n",
+ "\n",
+ " binary_score: str = Field(\n",
+ " description=\"Answer addresses the question, 'yes' or 'no'\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# LLM with function call\n",
+ "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
+ "structured_llm_grader = llm.with_structured_output(GradeAnswer)\n",
+ "\n",
+ "# Prompt\n",
+ "system = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n",
+ " Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\n",
+ "answer_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"system\", system),\n",
+ " (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "answer_grader = answer_prompt | structured_llm_grader\n",
+ "answer_grader.invoke({\"question\": question, \"generation\": generation})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "9d75f1d7-a47a-4577-bb0d-84b504b0867e",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "\"What is the role of memory in an agent's functioning?\""
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Question Re-writer\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
+ "\n",
+ "# Prompt\n",
+ "system = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n",
+ " for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\n",
+ "re_write_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"system\", system),\n",
+ " (\n",
+ " \"human\",\n",
+ " \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n",
+ " ),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "question_rewriter = re_write_prompt | llm | StrOutputParser()\n",
+ "question_rewriter.invoke({\"question\": question})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d07c0b31-b919-4498-869f-9673125c2473",
+ "metadata": {},
+ "source": [
+ "## Web Search Tool"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "01d829bb-1074-4976-b650-ead41dcb9788",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "### Search\n",
+ "\n",
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "\n",
+ "web_search_tool = TavilySearchResults(k=3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "efbbff0e-8843-45bb-b2ff-137bef707ef4",
+ "metadata": {},
+ "source": [
+ "## Construct the Graph \n",
+ "\n",
+ "Capture the flow in as a graph.\n",
+ "\n",
+ "### Define Graph State"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "e723fcdb-06e6-402d-912e-899795b78408",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from typing import List\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "\n",
+ "class GraphState(TypedDict):\n",
+ " \"\"\"\n",
+ " Represents the state of our graph.\n",
+ "\n",
+ " Attributes:\n",
+ " question: question\n",
+ " generation: LLM generation\n",
+ " documents: list of documents\n",
+ " \"\"\"\n",
+ "\n",
+ " question: str\n",
+ " generation: str\n",
+ " documents: List[str]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7e2d6c0d-42e8-4399-9751-e315be16607a",
+ "metadata": {},
+ "source": [
+ "### Define Graph Flow "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "b76b5ec3-0720-443d-85b1-c0e79659ca0a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langchain.schema import Document\n",
+ "\n",
+ "\n",
+ "def retrieve(state):\n",
+ " \"\"\"\n",
+ " Retrieve documents\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, documents, that contains retrieved documents\n",
+ " \"\"\"\n",
+ " print(\"---RETRIEVE---\")\n",
+ " question = state[\"question\"]\n",
+ "\n",
+ " # Retrieval\n",
+ " documents = retriever.invoke(question)\n",
+ " return {\"documents\": documents, \"question\": question}\n",
+ "\n",
+ "\n",
+ "def generate(state):\n",
+ " \"\"\"\n",
+ " Generate answer\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, generation, that contains LLM generation\n",
+ " \"\"\"\n",
+ " print(\"---GENERATE---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # RAG generation\n",
+ " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
+ " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
+ "\n",
+ "\n",
+ "def grade_documents(state):\n",
+ " \"\"\"\n",
+ " Determines whether the retrieved documents are relevant to the question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates documents key with only filtered relevant documents\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # Score each doc\n",
+ " filtered_docs = []\n",
+ " for d in documents:\n",
+ " score = retrieval_grader.invoke(\n",
+ " {\"question\": question, \"document\": d.page_content}\n",
+ " )\n",
+ " grade = score.binary_score\n",
+ " if grade == \"yes\":\n",
+ " print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
+ " filtered_docs.append(d)\n",
+ " else:\n",
+ " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n",
+ " continue\n",
+ " return {\"documents\": filtered_docs, \"question\": question}\n",
+ "\n",
+ "\n",
+ "def transform_query(state):\n",
+ " \"\"\"\n",
+ " Transform the query to produce a better question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates question key with a re-phrased question\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---TRANSFORM QUERY---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # Re-write question\n",
+ " better_question = question_rewriter.invoke({\"question\": question})\n",
+ " return {\"documents\": documents, \"question\": better_question}\n",
+ "\n",
+ "\n",
+ "def web_search(state):\n",
+ " \"\"\"\n",
+ " Web search based on the re-phrased question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates documents key with appended web results\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---WEB SEARCH---\")\n",
+ " question = state[\"question\"]\n",
+ "\n",
+ " # Web search\n",
+ " docs = web_search_tool.invoke({\"query\": question})\n",
+ " web_results = \"\\n\".join([d[\"content\"] for d in docs])\n",
+ " web_results = Document(page_content=web_results)\n",
+ "\n",
+ " return {\"documents\": web_results, \"question\": question}\n",
+ "\n",
+ "\n",
+ "### Edges ###\n",
+ "\n",
+ "\n",
+ "def route_question(state):\n",
+ " \"\"\"\n",
+ " Route question to web search or RAG.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---ROUTE QUESTION---\")\n",
+ " question = state[\"question\"]\n",
+ " source = question_router.invoke({\"question\": question})\n",
+ " if source.datasource == \"web_search\":\n",
+ " print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n",
+ " return \"web_search\"\n",
+ " elif source.datasource == \"vectorstore\":\n",
+ " print(\"---ROUTE QUESTION TO RAG---\")\n",
+ " return \"vectorstore\"\n",
+ "\n",
+ "\n",
+ "def decide_to_generate(state):\n",
+ " \"\"\"\n",
+ " Determines whether to generate an answer, or re-generate a question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Binary decision for next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---ASSESS GRADED DOCUMENTS---\")\n",
+ " state[\"question\"]\n",
+ " filtered_documents = state[\"documents\"]\n",
+ "\n",
+ " if not filtered_documents:\n",
+ " # All documents have been filtered check_relevance\n",
+ " # We will re-generate a new query\n",
+ " print(\n",
+ " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
+ " )\n",
+ " return \"transform_query\"\n",
+ " else:\n",
+ " # We have relevant documents, so generate answer\n",
+ " print(\"---DECISION: GENERATE---\")\n",
+ " return \"generate\"\n",
+ "\n",
+ "\n",
+ "def grade_generation_v_documents_and_question(state):\n",
+ " \"\"\"\n",
+ " Determines whether the generation is grounded in the document and answers question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Decision for next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---CHECK HALLUCINATIONS---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ " generation = state[\"generation\"]\n",
+ "\n",
+ " score = hallucination_grader.invoke(\n",
+ " {\"documents\": documents, \"generation\": generation}\n",
+ " )\n",
+ " grade = score.binary_score\n",
+ "\n",
+ " # Check hallucination\n",
+ " if grade == \"yes\":\n",
+ " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n",
+ " # Check question-answering\n",
+ " print(\"---GRADE GENERATION vs QUESTION---\")\n",
+ " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
+ " grade = score.binary_score\n",
+ " if grade == \"yes\":\n",
+ " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
+ " return \"useful\"\n",
+ " else:\n",
+ " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n",
+ " return \"not useful\"\n",
+ " else:\n",
+ " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n",
+ " return \"not supported\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3ab01f36-5628-49ab-bfd3-84bb6f1a1b0f",
+ "metadata": {},
+ "source": [
+ "### Compile Graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "67854e07-9293-4c3c-bf9a-bc9a605570ee",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langgraph.graph import END, StateGraph, START\n",
+ "\n",
+ "workflow = StateGraph(GraphState)\n",
+ "\n",
+ "# Define the nodes\n",
+ "workflow.add_node(\"web_search\", web_search) # web search\n",
+ "workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
+ "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
+ "workflow.add_node(\"generate\", generate) # generate\n",
+ "workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
+ "\n",
+ "# Build graph\n",
+ "workflow.add_conditional_edges(\n",
+ " START,\n",
+ " route_question,\n",
+ " {\n",
+ " \"web_search\": \"web_search\",\n",
+ " \"vectorstore\": \"retrieve\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"web_search\", \"generate\")\n",
+ "workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"grade_documents\",\n",
+ " decide_to_generate,\n",
+ " {\n",
+ " \"transform_query\": \"transform_query\",\n",
+ " \"generate\": \"generate\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"transform_query\", \"retrieve\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"generate\",\n",
+ " grade_generation_v_documents_and_question,\n",
+ " {\n",
+ " \"not supported\": \"generate\",\n",
+ " \"useful\": END,\n",
+ " \"not useful\": \"transform_query\",\n",
+ " },\n",
+ ")\n",
+ "\n",
+ "# Compile\n",
+ "app = workflow.compile()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "85bce541",
+ "metadata": {},
+ "source": [
+ "## Use Graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "29acc541-d726-4b75-84d1-a215845fe88a",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "---ROUTE QUESTION---\n",
+ "---ROUTE QUESTION TO WEB SEARCH---\n",
+ "---WEB SEARCH---\n",
+ "\"Node 'web_search':\"\n",
+ "'\\n---\\n'\n",
+ "---GENERATE---\n",
+ "---CHECK HALLUCINATIONS---\n",
+ "---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\n",
+ "---GRADE GENERATION vs QUESTION---\n",
+ "---DECISION: GENERATION ADDRESSES QUESTION---\n",
+ "\"Node 'generate':\"\n",
+ "'\\n---\\n'\n",
+ "('It is expected that the Chicago Bears could have the opportunity to draft '\n",
+ " 'the first defensive player in the 2024 NFL draft. The Bears have the first '\n",
+ " 'overall pick in the draft, giving them a prime position to select top '\n",
+ " 'talent. The top wide receiver Marvin Harrison Jr. from Ohio State is also '\n",
+ " 'mentioned as a potential pick for the Cardinals.')\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pprint import pprint\n",
+ "\n",
+ "# Run\n",
+ "inputs = {\n",
+ " \"question\": \"What player at the Bears expected to draft first in the 2024 NFL draft?\"\n",
+ "}\n",
+ "for output in app.stream(inputs):\n",
+ " for key, value in output.items():\n",
+ " # Node\n",
+ " pprint(f\"Node '{key}':\")\n",
+ " # Optional: print full state at each node\n",
+ " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
+ " pprint(\"\\n---\\n\")\n",
+ "\n",
+ "# Final generation\n",
+ "pprint(value[\"generation\"])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "11fddd00-58bf-4910-bf36-be9e5bfba778",
+ "metadata": {},
+ "source": [
+ "Trace: \n",
+ "\n",
+ "https://smith.langchain.com/public/7e3aa7e5-c51f-45c2-bc66-b34f17ff2263/r"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "69a985dd-03c6-45af-a67b-b15746a2cb5f",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "---ROUTE QUESTION---\n",
+ "---ROUTE QUESTION TO RAG---\n",
+ "---RETRIEVE---\n",
+ "\"Node 'retrieve':\"\n",
+ "'\\n---\\n'\n",
+ "---CHECK DOCUMENT RELEVANCE TO QUESTION---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---GRADE: DOCUMENT NOT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---ASSESS GRADED DOCUMENTS---\n",
+ "---DECISION: GENERATE---\n",
+ "\"Node 'grade_documents':\"\n",
+ "'\\n---\\n'\n",
+ "---GENERATE---\n",
+ "---CHECK HALLUCINATIONS---\n",
+ "---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\n",
+ "---GRADE GENERATION vs QUESTION---\n",
+ "---DECISION: GENERATION ADDRESSES QUESTION---\n",
+ "\"Node 'generate':\"\n",
+ "'\\n---\\n'\n",
+ "('The types of agent memory include Sensory Memory, Short-Term Memory (STM) or '\n",
+ " 'Working Memory, and Long-Term Memory (LTM) with subtypes of Explicit / '\n",
+ " 'declarative memory and Implicit / procedural memory. Sensory memory retains '\n",
+ " 'sensory information briefly, STM stores information for cognitive tasks, and '\n",
+ " 'LTM stores information for a long time with different types of memories.')\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Run\n",
+ "inputs = {\"question\": \"What are the types of agent memory?\"}\n",
+ "for output in app.stream(inputs):\n",
+ " for key, value in output.items():\n",
+ " # Node\n",
+ " pprint(f\"Node '{key}':\")\n",
+ " # Optional: print full state at each node\n",
+ " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
+ " pprint(\"\\n---\\n\")\n",
+ "\n",
+ "# Final generation\n",
+ "pprint(value[\"generation\"])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ebf41097-fc4c-4072-95b3-e7e07731ada1",
+ "metadata": {},
+ "source": [
+ "Trace: \n",
+ "\n",
+ "https://smith.langchain.com/public/fdf0a180-6d15-4d09-bb92-f84f2105ca51/r"
+ ]
+ }
+ ],
+ "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.8"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/langgraph/source/examples/rag/langgraph_adaptive_rag_cohere.ipynb b/langgraph/source/examples/rag/langgraph_adaptive_rag_cohere.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..e3732931b2aeba7ef9241a7692aa5e347a6dfd99
--- /dev/null
+++ b/langgraph/source/examples/rag/langgraph_adaptive_rag_cohere.ipynb
@@ -0,0 +1,1103 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "ba8a450f",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
+ ]
+ },
+ {
+ "attachments": {
+ "2a4ecdd2-280d-4311-a2cd-cd6138090be9.png": {
+ "image/png": "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gAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkgDBpCBB0gwCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggYEcBgkl2vKv0CQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIkkBYkNqhGQQQQAABBJpVoHjJYilR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QAABBBBAAAEEEEAAAQQQQAABBBBAIHABgkmB23EkAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIGB7gf9n7zzgq6iaNj7pBZIASeih917kFRQQFAEBBQsCKqJYUD8rgh0FsTewUhQURaqgIqhUQWkiKL13EgiEkEB65ZvnJHtz0wtJSMIz/vbu2bOn/vcSk/vcmaGYVOYfMTdIAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAgUnQDGp4OzYkwRIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgATKPAGKSWX+EXODJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJFBwAhSTCs6OPUmABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEigzBOgmFTmHzE3SAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIFJ0AxqeDs2JMESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAEyjwBikll/hFzgyRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRQcAIUkwrOjj1JgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIoMwToJhU5h8xN0gCJEACJEACJEACJEACJEACJEACJEACJEACJEACZZtA4sVkWR1+VMITY8v2Rrm7bAlEJydIUNyFbO9f6o2/IwLlWOz5Sx2m1PZ3uKhWalfPhZMACZAACZAACZAACZAACZAACZAACZAACZAACZAACVzxBMYeWiph8ZFSztlD3mrY54rnUZwAIpLi5U8V8m6oWE/cHZ2Lc2rbXHujz8qkY2vMdYCnvzQvX02al6ss1d28xNnh0n1qvj+9VTadO2TGf6fxLeLh6GKbO7fC2YRo8XPxzK1Zib9/eZ5sicfCBZIACZAACZAACZAACZAACZAACZAACZAACZAACZAACZQGAmHqjQQhCZZ4Mak0LLlMrXFq4EY5Hh0iYSqa3FO1zWXZ24bzx2zzntC14Pjd1DhIZY8K0qNSQ/mfd4A42Frlr7D1/Albh7x65+xXgeurwA0Sp2JbeRU5xzboJS4OTrZxSluBYlJpe2JcLwmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQgI3Ab6H7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QmRskpwJj5U43St2VsnLVedwzXXd1hQIWwdRClJQboywV2hGNX098pzPyZonL2eEtkO+pwgN2+fp5iTVwV8FORoJkAAJkAAJkAAJFAWBtm3bSo8ePeT9998viuE5JgmQAAmQQBkmQDGpDD9cbo0ESIAESIAESIAESIAESIAESIAESIAESIAEQOCOO+4QHx8fmTZtGoGQAAmQAAmQQL4JZB3bJN/DsAMJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkEBJJPD8889LZGSkzJgxoyQuj2siARIgARIoBQQoJpWCh8QlkgAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkEBBCLzzzjuyZMkSWbFihZQrV64gQ7APCZAACZAACQjFJL4JSIAESIAESIAESIAESIAESIAESIAESIAESKAMEpgwYYJMmjRJli9fLlWrVi2DO+SWSIAESIAEiosAs/sWF2nOQwIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAKpBPYfPyL/7txeZDwgJE2cOFEWLlwojRo1KrJ5ODAJkAAJkMCVQYBi0pXxnLlLEiABEiABEiABEiABEiABEiABEiABEiCBEkLgqVdflBu7dJNb+94sHa7tJBs2bijUlb377rtGSIJXUvv27Qt1bA5GAiRAAiRwZRKgmHRlPnfumgRIgARIgARIgARIgARIgARIgARIgARIoJgJXLhwQa7r1UN+mjFLKlWrLJ3vvk0SkxJl8KDB8uGEjwplNa+//rp88cUX8vLLL0ufPn0KZUwOQgIkQAIkQAIOF9WIgQRIgARIgARIgARIgARIgARIgARIgARIgARIoOgI7N69W4Y9OFwuhJ+XAc8/Jh0G9LRNtnTSd7JMjx49b5QJH34k3t7etnv5KUBAmjlzpgwbNkwgKtFIgARIgARIoLAIUEwqLJIchwRIgARIgARIgARIgARIgARIgARIgARIgASyIAAh6c5Bd0rlRnXl/o/HiodX+UytgvYekvmvfiTlHJ1l/tx5+RaUxowZI99++63ccMMNMn369Ezjs4IESIAESIAELoUAxaRLoce+JEACJEACJEACJEACJEACJEACJEACJEACJJADASMk3XmndLpngPR6dGgOLUViIiJlyXtTZOcfG2Te3LnSrFmzHNtbN8eNG2cEpBYtWsiSJUusap5JgARIgARIoNAIUEwqNJQc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gAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEvQDAp9O8xV4gAAggggAACCCCAAAIIIIAAAggggAACCCCAAAII+CxAMMlnOhoigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAqEv8P9hwox7hyJ7QAAAAABJRU5ErkJggg=="
+ }
+ },
+ "cell_type": "markdown",
+ "id": "5afcaed0-3d55-4e1f-95d3-c32c751c29d8",
+ "metadata": {
+ "id": "5afcaed0-3d55-4e1f-95d3-c32c751c29d8"
+ },
+ "source": [
+ "# Adaptive RAG Cohere Command R\n",
+ "\n",
+ "Adaptive RAG is a strategy for RAG that unites (1) [query analysis](https://blog.langchain.dev/query-construction/) with (2) [active / self-corrective RAG](https://blog.langchain.dev/agentic-rag-with-langgraph/).\n",
+ "\n",
+ "In the paper, they report query analysis to route across:\n",
+ "\n",
+ "* No Retrieval (LLM answers)\n",
+ "* Single-shot RAG\n",
+ "* Iterative RAG\n",
+ "\n",
+ "Let's build on this to perform query analysis to route across some more interesting cases:\n",
+ "\n",
+ "* No Retrieval (LLM answers)\n",
+ "* Web-search\n",
+ "* Iterative RAG\n",
+ "\n",
+ "We'll use [Command R](https://cohere.com/blog/command-r), a recent release from Cohere that:\n",
+ "\n",
+ "* Has strong accuracy on RAG and Tool Use\n",
+ "* Has 128k context\n",
+ "* Has low latency \n",
+ " \n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a85501ca-eb89-4795-aeab-cdab050ead6b",
+ "metadata": {
+ "id": "a85501ca-eb89-4795-aeab-cdab050ead6b"
+ },
+ "source": [
+ "# Environment"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f6c329ba-cb85-4576-9828-4f2ac648d1a6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "! pip install --quiet langchain langchain_cohere langchain-openai tiktoken langchainhub chromadb langgraph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "222f204d-956f-4128-b597-2c698120edda",
+ "metadata": {
+ "id": "222f204d-956f-4128-b597-2c698120edda"
+ },
+ "outputs": [],
+ "source": [
+ "### LLMs\n",
+ "import os\n",
+ "\n",
+ "os.environ[\"COHERE_API_KEY\"] = \"\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "08edba00-988a-478b-96fc-ae0199cbef49",
+ "metadata": {
+ "id": "08edba00-988a-478b-96fc-ae0199cbef49"
+ },
+ "outputs": [],
+ "source": [
+ "# ### Tracing (optional)\n",
+ "# os.environ['LANGCHAIN_TRACING_V2'] = 'true'\n",
+ "# os.environ['LANGCHAIN_ENDPOINT'] = 'https://api.smith.langchain.com'\n",
+ "# os.environ['LANGCHAIN_API_KEY'] =''"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9ac1c2cd-81fb-40eb-8ba1-e9197800cba6",
+ "metadata": {
+ "id": "9ac1c2cd-81fb-40eb-8ba1-e9197800cba6"
+ },
+ "source": [
+ "## Index"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "b224e5ba-50ca-495a-a7fa-0f75a080e03c",
+ "metadata": {
+ "id": "b224e5ba-50ca-495a-a7fa-0f75a080e03c"
+ },
+ "outputs": [],
+ "source": [
+ "### Build Index\n",
+ "\n",
+ "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
+ "from langchain_cohere import CohereEmbeddings\n",
+ "from langchain_community.document_loaders import WebBaseLoader\n",
+ "from langchain_community.vectorstores import Chroma\n",
+ "\n",
+ "# Set embeddings\n",
+ "embd = CohereEmbeddings()\n",
+ "\n",
+ "# Docs to index\n",
+ "urls = [\n",
+ " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
+ "]\n",
+ "\n",
+ "# Load\n",
+ "docs = [WebBaseLoader(url).load() for url in urls]\n",
+ "docs_list = [item for sublist in docs for item in sublist]\n",
+ "\n",
+ "# Split\n",
+ "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
+ " chunk_size=512, chunk_overlap=0\n",
+ ")\n",
+ "doc_splits = text_splitter.split_documents(docs_list)\n",
+ "\n",
+ "# Add to vectorstore\n",
+ "vectorstore = Chroma.from_documents(\n",
+ " documents=doc_splits,\n",
+ " embedding=embd,\n",
+ ")\n",
+ "\n",
+ "retriever = vectorstore.as_retriever()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0f52b427-750c-40f8-8893-e9caab3afd8d",
+ "metadata": {
+ "id": "0f52b427-750c-40f8-8893-e9caab3afd8d"
+ },
+ "source": [
+ "## LLMs"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "H5CztTqsBOTZ",
+ "metadata": {
+ "id": "H5CztTqsBOTZ"
+ },
+ "source": [
+ "We use a router to pick between tools. \n",
+ " \n",
+ "Cohere model decides which tool(s) to call, as well as the how to query them."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "bYK-e0diGdPf",
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "bYK-e0diGdPf",
+ "outputId": "895a8ea5-57ee-49fe-ef28-277eac8a7bb7"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[{'id': 'f811e3b9-052e-49db-a234-5fc3efbcc5ba', 'function': {'name': 'web_search', 'arguments': '{\"query\": \"NFL draft bears first pick\"}'}, 'type': 'function'}]\n",
+ "[{'id': '4bc53113-8f32-4d6d-ac9b-c07ef9aae9fd', 'function': {'name': 'vectorstore', 'arguments': '{\"query\": \"types of agent memory\"}'}, 'type': 'function'}]\n",
+ "False\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Router\n",
+ "\n",
+ "from langchain_cohere import ChatCohere\n",
+ "from langchain_core.prompts import ChatPromptTemplate\n",
+ "from langchain_core.pydantic_v1 import BaseModel, Field\n",
+ "\n",
+ "\n",
+ "# Data model\n",
+ "class web_search(BaseModel):\n",
+ " \"\"\"\n",
+ " The internet. Use web_search for questions that are related to anything else than agents, prompt engineering, and adversarial attacks.\n",
+ " \"\"\"\n",
+ "\n",
+ " query: str = Field(description=\"The query to use when searching the internet.\")\n",
+ "\n",
+ "\n",
+ "class vectorstore(BaseModel):\n",
+ " \"\"\"\n",
+ " A vectorstore containing documents related to agents, prompt engineering, and adversarial attacks. Use the vectorstore for questions on these topics.\n",
+ " \"\"\"\n",
+ "\n",
+ " query: str = Field(description=\"The query to use when searching the vectorstore.\")\n",
+ "\n",
+ "\n",
+ "# Preamble\n",
+ "preamble = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\n",
+ "The vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\n",
+ "Use the vectorstore for questions on these topics. Otherwise, use web-search.\"\"\"\n",
+ "\n",
+ "# LLM with tool use and preamble\n",
+ "llm = ChatCohere(model=\"command-r\", temperature=0)\n",
+ "structured_llm_router = llm.bind_tools(\n",
+ " tools=[web_search, vectorstore], preamble=preamble\n",
+ ")\n",
+ "\n",
+ "# Prompt\n",
+ "route_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"human\", \"{question}\"),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "question_router = route_prompt | structured_llm_router\n",
+ "response = question_router.invoke(\n",
+ " {\"question\": \"Who will the Bears draft first in the NFL draft?\"}\n",
+ ")\n",
+ "print(response.response_metadata[\"tool_calls\"])\n",
+ "response = question_router.invoke({\"question\": \"What are the types of agent memory?\"})\n",
+ "print(response.response_metadata[\"tool_calls\"])\n",
+ "response = question_router.invoke({\"question\": \"Hi how are you?\"})\n",
+ "print(\"tool_calls\" in response.response_metadata)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "oaLWNbWxBgjE",
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "oaLWNbWxBgjE",
+ "outputId": "57a5c27b-044b-4df5-f55d-7bf23d3976d1"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "binary_score='yes'\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Retrieval Grader\n",
+ "\n",
+ "\n",
+ "# Data model\n",
+ "class GradeDocuments(BaseModel):\n",
+ " \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n",
+ "\n",
+ " binary_score: str = Field(\n",
+ " description=\"Documents are relevant to the question, 'yes' or 'no'\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# Prompt\n",
+ "preamble = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n\n",
+ "If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n",
+ "Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n",
+ "\n",
+ "# LLM with function call\n",
+ "llm = ChatCohere(model=\"command-r\", temperature=0)\n",
+ "structured_llm_grader = llm.with_structured_output(GradeDocuments, preamble=preamble)\n",
+ "\n",
+ "grade_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "retrieval_grader = grade_prompt | structured_llm_grader\n",
+ "question = \"types of agent memory\"\n",
+ "docs = retriever.invoke(question)\n",
+ "doc_txt = docs[1].page_content\n",
+ "response = retrieval_grader.invoke({\"question\": question, \"document\": doc_txt})\n",
+ "print(response)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "D43a7vM4EElX",
+ "metadata": {
+ "id": "D43a7vM4EElX"
+ },
+ "source": [
+ "Generate"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "BTIUdjRMEq_h",
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "BTIUdjRMEq_h",
+ "outputId": "11a99f62-2449-45db-9281-5bfd60e3c966"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "There are three types of agent memory: sensory memory, short-term memory, and long-term memory.\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Generate\n",
+ "\n",
+ "from langchain_core.messages import HumanMessage\n",
+ "from langchain_core.output_parsers import StrOutputParser\n",
+ "\n",
+ "# Preamble\n",
+ "preamble = \"\"\"You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.\"\"\"\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatCohere(model_name=\"command-r\", temperature=0).bind(preamble=preamble)\n",
+ "\n",
+ "\n",
+ "# Prompt\n",
+ "def prompt(x):\n",
+ " return ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " HumanMessage(\n",
+ " f\"Question: {x['question']} \\nAnswer: \",\n",
+ " additional_kwargs={\"documents\": x[\"documents\"]},\n",
+ " )\n",
+ " ]\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# Chain\n",
+ "rag_chain = prompt | llm | StrOutputParser()\n",
+ "\n",
+ "# Run\n",
+ "generation = rag_chain.invoke({\"documents\": docs, \"question\": question})\n",
+ "print(generation)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "bc000a7d-84b6-4eb2-88ad-65cc62a44431",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "I don't have feelings as an AI chatbot, but I'm here to assist you with any queries or concerns you may have. How can I help you today?\n"
+ ]
+ }
+ ],
+ "source": [
+ "### LLM fallback\n",
+ "\n",
+ "from langchain_core.output_parsers import StrOutputParser\n",
+ "\n",
+ "# Preamble\n",
+ "preamble = \"\"\"You are an assistant for question-answering tasks. Answer the question based upon your knowledge. Use three sentences maximum and keep the answer concise.\"\"\"\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatCohere(model_name=\"command-r\", temperature=0).bind(preamble=preamble)\n",
+ "\n",
+ "\n",
+ "# Prompt\n",
+ "def prompt(x):\n",
+ " return ChatPromptTemplate.from_messages(\n",
+ " [HumanMessage(f\"Question: {x['question']} \\nAnswer: \")]\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# Chain\n",
+ "llm_chain = prompt | llm | StrOutputParser()\n",
+ "\n",
+ "# Run\n",
+ "question = \"Hi how are you?\"\n",
+ "generation = llm_chain.invoke({\"question\": question})\n",
+ "print(generation)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "y0msuR2DHQkY",
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "y0msuR2DHQkY",
+ "outputId": "f0e91c2a-5542-45c0-a7a6-60453e0b2bc4"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "GradeHallucinations(binary_score='yes')"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Hallucination Grader\n",
+ "\n",
+ "\n",
+ "# Data model\n",
+ "class GradeHallucinations(BaseModel):\n",
+ " \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n",
+ "\n",
+ " binary_score: str = Field(\n",
+ " description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# Preamble\n",
+ "preamble = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n\n",
+ "Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\n",
+ "\n",
+ "# LLM with function call\n",
+ "llm = ChatCohere(model=\"command-r\", temperature=0)\n",
+ "structured_llm_grader = llm.with_structured_output(\n",
+ " GradeHallucinations, preamble=preamble\n",
+ ")\n",
+ "\n",
+ "# Prompt\n",
+ "hallucination_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " # (\"system\", system),\n",
+ " (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "hallucination_grader = hallucination_prompt | structured_llm_grader\n",
+ "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "f0c08d14-77a0-4eed-b882-2d636abb22a3",
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "f0c08d14-77a0-4eed-b882-2d636abb22a3",
+ "outputId": "c4f88c9a-65fd-4dad-e739-3c9bd547a9f5"
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "GradeAnswer(binary_score='yes')"
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Answer Grader\n",
+ "\n",
+ "\n",
+ "# Data model\n",
+ "class GradeAnswer(BaseModel):\n",
+ " \"\"\"Binary score to assess answer addresses question.\"\"\"\n",
+ "\n",
+ " binary_score: str = Field(\n",
+ " description=\"Answer addresses the question, 'yes' or 'no'\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# Preamble\n",
+ "preamble = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n\n",
+ "Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\n",
+ "\n",
+ "# LLM with function call\n",
+ "llm = ChatCohere(model=\"command-r\", temperature=0)\n",
+ "structured_llm_grader = llm.with_structured_output(GradeAnswer, preamble=preamble)\n",
+ "\n",
+ "# Prompt\n",
+ "answer_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "answer_grader = answer_prompt | structured_llm_grader\n",
+ "answer_grader.invoke({\"question\": question, \"generation\": generation})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d07c0b31-b919-4498-869f-9673125c2473",
+ "metadata": {
+ "id": "d07c0b31-b919-4498-869f-9673125c2473"
+ },
+ "source": [
+ "## Web Search Tool"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "01d829bb-1074-4976-b650-ead41dcb9788",
+ "metadata": {
+ "id": "01d829bb-1074-4976-b650-ead41dcb9788"
+ },
+ "outputs": [],
+ "source": [
+ "### Search\n",
+ "# os.environ['TAVILY_API_KEY'] =''\n",
+ "\n",
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "\n",
+ "web_search_tool = TavilySearchResults()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "efbbff0e-8843-45bb-b2ff-137bef707ef4",
+ "metadata": {
+ "id": "efbbff0e-8843-45bb-b2ff-137bef707ef4"
+ },
+ "source": [
+ "# Graph\n",
+ "\n",
+ "Capture the flow in as a graph.\n",
+ "\n",
+ "## Graph state"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "e723fcdb-06e6-402d-912e-899795b78408",
+ "metadata": {
+ "id": "e723fcdb-06e6-402d-912e-899795b78408"
+ },
+ "outputs": [],
+ "source": [
+ "from typing import List\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "\n",
+ "class GraphState(TypedDict):\n",
+ " \"\"\"|\n",
+ " Represents the state of our graph.\n",
+ "\n",
+ " Attributes:\n",
+ " question: question\n",
+ " generation: LLM generation\n",
+ " documents: list of documents\n",
+ " \"\"\"\n",
+ "\n",
+ " question: str\n",
+ " generation: str\n",
+ " documents: List[str]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7e2d6c0d-42e8-4399-9751-e315be16607a",
+ "metadata": {
+ "id": "7e2d6c0d-42e8-4399-9751-e315be16607a"
+ },
+ "source": [
+ "## Graph Flow"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "b76b5ec3-0720-443d-85b1-c0e79659ca0a",
+ "metadata": {
+ "id": "b76b5ec3-0720-443d-85b1-c0e79659ca0a"
+ },
+ "outputs": [],
+ "source": [
+ "from langchain.schema import Document\n",
+ "\n",
+ "\n",
+ "def retrieve(state):\n",
+ " \"\"\"\n",
+ " Retrieve documents\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, documents, that contains retrieved documents\n",
+ " \"\"\"\n",
+ " print(\"---RETRIEVE---\")\n",
+ " question = state[\"question\"]\n",
+ "\n",
+ " # Retrieval\n",
+ " documents = retriever.invoke(question)\n",
+ " return {\"documents\": documents, \"question\": question}\n",
+ "\n",
+ "\n",
+ "def llm_fallback(state):\n",
+ " \"\"\"\n",
+ " Generate answer using the LLM w/o vectorstore\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, generation, that contains LLM generation\n",
+ " \"\"\"\n",
+ " print(\"---LLM Fallback---\")\n",
+ " question = state[\"question\"]\n",
+ " generation = llm_chain.invoke({\"question\": question})\n",
+ " return {\"question\": question, \"generation\": generation}\n",
+ "\n",
+ "\n",
+ "def generate(state):\n",
+ " \"\"\"\n",
+ " Generate answer using the vectorstore\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, generation, that contains LLM generation\n",
+ " \"\"\"\n",
+ " print(\"---GENERATE---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ " if not isinstance(documents, list):\n",
+ " documents = [documents]\n",
+ "\n",
+ " # RAG generation\n",
+ " generation = rag_chain.invoke({\"documents\": documents, \"question\": question})\n",
+ " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
+ "\n",
+ "\n",
+ "def grade_documents(state):\n",
+ " \"\"\"\n",
+ " Determines whether the retrieved documents are relevant to the question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates documents key with only filtered relevant documents\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # Score each doc\n",
+ " filtered_docs = []\n",
+ " for d in documents:\n",
+ " score = retrieval_grader.invoke(\n",
+ " {\"question\": question, \"document\": d.page_content}\n",
+ " )\n",
+ " grade = score.binary_score\n",
+ " if grade == \"yes\":\n",
+ " print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
+ " filtered_docs.append(d)\n",
+ " else:\n",
+ " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n",
+ " continue\n",
+ " return {\"documents\": filtered_docs, \"question\": question}\n",
+ "\n",
+ "\n",
+ "def web_search(state):\n",
+ " \"\"\"\n",
+ " Web search based on the re-phrased question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates documents key with appended web results\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---WEB SEARCH---\")\n",
+ " question = state[\"question\"]\n",
+ "\n",
+ " # Web search\n",
+ " docs = web_search_tool.invoke({\"query\": question})\n",
+ " web_results = \"\\n\".join([d[\"content\"] for d in docs])\n",
+ " web_results = Document(page_content=web_results)\n",
+ "\n",
+ " return {\"documents\": web_results, \"question\": question}\n",
+ "\n",
+ "\n",
+ "### Edges ###\n",
+ "\n",
+ "\n",
+ "def route_question(state):\n",
+ " \"\"\"\n",
+ " Route question to web search or RAG.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---ROUTE QUESTION---\")\n",
+ " question = state[\"question\"]\n",
+ " source = question_router.invoke({\"question\": question})\n",
+ "\n",
+ " # Fallback to LLM or raise error if no decision\n",
+ " if \"tool_calls\" not in source.additional_kwargs:\n",
+ " print(\"---ROUTE QUESTION TO LLM---\")\n",
+ " return \"llm_fallback\"\n",
+ " if len(source.additional_kwargs[\"tool_calls\"]) == 0:\n",
+ " raise \"Router could not decide source\"\n",
+ "\n",
+ " # Choose datasource\n",
+ " datasource = source.additional_kwargs[\"tool_calls\"][0][\"function\"][\"name\"]\n",
+ " if datasource == \"web_search\":\n",
+ " print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n",
+ " return \"web_search\"\n",
+ " elif datasource == \"vectorstore\":\n",
+ " print(\"---ROUTE QUESTION TO RAG---\")\n",
+ " return \"vectorstore\"\n",
+ " else:\n",
+ " print(\"---ROUTE QUESTION TO LLM---\")\n",
+ " return \"vectorstore\"\n",
+ "\n",
+ "\n",
+ "def decide_to_generate(state):\n",
+ " \"\"\"\n",
+ " Determines whether to generate an answer, or re-generate a question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Binary decision for next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---ASSESS GRADED DOCUMENTS---\")\n",
+ " state[\"question\"]\n",
+ " filtered_documents = state[\"documents\"]\n",
+ "\n",
+ " if not filtered_documents:\n",
+ " # All documents have been filtered check_relevance\n",
+ " # We will re-generate a new query\n",
+ " print(\"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, WEB SEARCH---\")\n",
+ " return \"web_search\"\n",
+ " else:\n",
+ " # We have relevant documents, so generate answer\n",
+ " print(\"---DECISION: GENERATE---\")\n",
+ " return \"generate\"\n",
+ "\n",
+ "\n",
+ "def grade_generation_v_documents_and_question(state):\n",
+ " \"\"\"\n",
+ " Determines whether the generation is grounded in the document and answers question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Decision for next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---CHECK HALLUCINATIONS---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ " generation = state[\"generation\"]\n",
+ "\n",
+ " score = hallucination_grader.invoke(\n",
+ " {\"documents\": documents, \"generation\": generation}\n",
+ " )\n",
+ " grade = score.binary_score\n",
+ "\n",
+ " # Check hallucination\n",
+ " if grade == \"yes\":\n",
+ " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n",
+ " # Check question-answering\n",
+ " print(\"---GRADE GENERATION vs QUESTION---\")\n",
+ " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
+ " grade = score.binary_score\n",
+ " if grade == \"yes\":\n",
+ " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
+ " return \"useful\"\n",
+ " else:\n",
+ " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n",
+ " return \"not useful\"\n",
+ " else:\n",
+ " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n",
+ " return \"not supported\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3ab01f36-5628-49ab-bfd3-84bb6f1a1b0f",
+ "metadata": {
+ "id": "3ab01f36-5628-49ab-bfd3-84bb6f1a1b0f"
+ },
+ "source": [
+ "## Build Graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "67854e07-9293-4c3c-bf9a-bc9a605570ee",
+ "metadata": {
+ "id": "67854e07-9293-4c3c-bf9a-bc9a605570ee"
+ },
+ "outputs": [],
+ "source": [
+ "import pprint\n",
+ "\n",
+ "from langgraph.graph import END, StateGraph, START\n",
+ "\n",
+ "workflow = StateGraph(GraphState)\n",
+ "\n",
+ "# Define the nodes\n",
+ "workflow.add_node(\"web_search\", web_search) # web search\n",
+ "workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
+ "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
+ "workflow.add_node(\"generate\", generate) # rag\n",
+ "workflow.add_node(\"llm_fallback\", llm_fallback) # llm\n",
+ "\n",
+ "# Build graph\n",
+ "workflow.add_conditional_edges(\n",
+ " START,\n",
+ " route_question,\n",
+ " {\n",
+ " \"web_search\": \"web_search\",\n",
+ " \"vectorstore\": \"retrieve\",\n",
+ " \"llm_fallback\": \"llm_fallback\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"web_search\", \"generate\")\n",
+ "workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"grade_documents\",\n",
+ " decide_to_generate,\n",
+ " {\n",
+ " \"web_search\": \"web_search\",\n",
+ " \"generate\": \"generate\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"generate\",\n",
+ " grade_generation_v_documents_and_question,\n",
+ " {\n",
+ " \"not supported\": \"generate\", # Hallucinations: re-generate\n",
+ " \"not useful\": \"web_search\", # Fails to answer question: fall-back to web-search\n",
+ " \"useful\": END,\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"llm_fallback\", END)\n",
+ "\n",
+ "# Compile\n",
+ "app = workflow.compile()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "29acc541-d726-4b75-84d1-a215845fe88a",
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "29acc541-d726-4b75-84d1-a215845fe88a",
+ "outputId": "47caec8e-54e3-4f89-dfbb-94fc034666f7"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "---ROUTE QUESTION---\n",
+ "---ROUTE QUESTION TO WEB SEARCH---\n",
+ "---WEB SEARCH---\n",
+ "\"Node 'web_search':\"\n",
+ "'\\n---\\n'\n",
+ "---GENERATE---\n",
+ "---CHECK HALLUCINATIONS---\n",
+ "---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\n",
+ "---GRADE GENERATION vs QUESTION---\n",
+ "---DECISION: GENERATION ADDRESSES QUESTION---\n",
+ "\"Node 'generate':\"\n",
+ "'\\n---\\n'\n",
+ "'The Bears are expected to draft Caleb Williams with their first pick.'\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Run\n",
+ "inputs = {\n",
+ " \"question\": \"What player are the Bears expected to draft first in the 2024 NFL draft?\"\n",
+ "}\n",
+ "for output in app.stream(inputs):\n",
+ " for key, value in output.items():\n",
+ " # Node\n",
+ " pprint.pprint(f\"Node '{key}':\")\n",
+ " # Optional: print full state at each node\n",
+ " pprint.pprint(\"\\n---\\n\")\n",
+ "\n",
+ "# Final generation\n",
+ "pprint.pprint(value[\"generation\"])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "11fddd00-58bf-4910-bf36-be9e5bfba778",
+ "metadata": {
+ "id": "11fddd00-58bf-4910-bf36-be9e5bfba778"
+ },
+ "source": [
+ "Trace:\n",
+ "\n",
+ "https://smith.langchain.com/public/623da7bb-84a7-4e53-a63e-7ccd77fb9be5/r"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "69a985dd-03c6-45af-a67b-b15746a2cb5f",
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "69a985dd-03c6-45af-a67b-b15746a2cb5f",
+ "outputId": "e5f799cc-6f36-494f-c8b2-192de1edb7fc"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "---ROUTE QUESTION---\n",
+ "---ROUTE QUESTION TO RAG---\n",
+ "---RETRIEVE---\n",
+ "\"Node 'retrieve':\"\n",
+ "'\\n---\\n'\n",
+ "---CHECK DOCUMENT RELEVANCE TO QUESTION---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---ASSESS GRADED DOCUMENTS---\n",
+ "---DECISION: GENERATE---\n",
+ "\"Node 'grade_documents':\"\n",
+ "'\\n---\\n'\n",
+ "---GENERATE---\n",
+ "---CHECK HALLUCINATIONS---\n",
+ "---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\n",
+ "---GRADE GENERATION vs QUESTION---\n",
+ "---DECISION: GENERATION ADDRESSES QUESTION---\n",
+ "\"Node 'generate':\"\n",
+ "'\\n---\\n'\n",
+ "'Sensory, short-term, and long-term memory.'\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Run\n",
+ "inputs = {\"question\": \"What are the types of agent memory?\"}\n",
+ "for output in app.stream(inputs):\n",
+ " for key, value in output.items():\n",
+ " # Node\n",
+ " pprint.pprint(f\"Node '{key}':\")\n",
+ " # Optional: print full state at each node\n",
+ " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
+ " pprint.pprint(\"\\n---\\n\")\n",
+ "\n",
+ "# Final generation\n",
+ "pprint.pprint(value[\"generation\"])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ebf41097-fc4c-4072-95b3-e7e07731ada1",
+ "metadata": {
+ "id": "ebf41097-fc4c-4072-95b3-e7e07731ada1"
+ },
+ "source": [
+ "Trace:\n",
+ "\n",
+ "https://smith.langchain.com/public/57f3973b-6879-4fbe-ae31-9ae524c3a697/r"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "qPwP_2PNiOjQ",
+ "metadata": {
+ "id": "qPwP_2PNiOjQ"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "---ROUTE QUESTION---\n",
+ "---ROUTE QUESTION TO LLM---\n",
+ "---LLM Fallback---\n",
+ "\"Node 'llm_fallback':\"\n",
+ "'\\n---\\n'\n",
+ "(\"I don't have feelings as an AI assistant, but I'm here to help you with your \"\n",
+ " 'queries. How can I assist you today?')\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Run\n",
+ "inputs = {\"question\": \"Hello, how are you today?\"}\n",
+ "for output in app.stream(inputs):\n",
+ " for key, value in output.items():\n",
+ " # Node\n",
+ " pprint.pprint(f\"Node '{key}':\")\n",
+ " # Optional: print full state at each node\n",
+ " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
+ " pprint.pprint(\"\\n---\\n\")\n",
+ "\n",
+ "# Final generation\n",
+ "pprint.pprint(value[\"generation\"])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4107c8a4-6171-4c1b-840a-77a3d09f84fc",
+ "metadata": {},
+ "source": [
+ "Trace: \n",
+ "\n",
+ "https://smith.langchain.com/public/1f628ee4-8d2d-451e-aeb1-5d5e0ede2b4f/r"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ce3cda0a-c4bd-41ea-830b-d992f27fde15",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "colab": {
+ "provenance": []
+ },
+ "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.8"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/langgraph/source/examples/rag/langgraph_adaptive_rag_local.ipynb b/langgraph/source/examples/rag/langgraph_adaptive_rag_local.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..0b289636ba7d72497a6ab7f7da66ccdd6e92a9f8
--- /dev/null
+++ b/langgraph/source/examples/rag/langgraph_adaptive_rag_local.ipynb
@@ -0,0 +1,846 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "39b26b09",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
+ ]
+ },
+ {
+ "attachments": {
+ "3755396d-c4a8-45bd-87d4-00cb56339fe5.png": {
+ "image/png": "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AiRAAiRAAiRAAiRAAr2IAEWHXnSzOFQSIAESIAESIAESIAESIAESIAES6E0EKDr0prvFsZIACZAACZAACZAACZAACZAACZBALyJA0aEX3SwOlQRIgARIgARIgARIgARIgARIgAR6EwGKDr3pbnGsJEACJEACJEACJEACJEACJEACJNCLCFB06EU3i0MlARIgARIgARIgARIgARIgARIggd5EgKJDb7pbHCsJkAAJkAAJkAAJkAAJkAAJkAAJ9CICFB160c3iUEmABEiABEiABEiABEiABEiABEigNxGg6NCb7hbHSgIkQAIkQAIkQAIkQAIkQAIkQAK9iABFh150szhUEiABEiABEiABEiABEiABEiABEuhNBCg69Ka7xbGSAAmQAAmQAAmQAAmQAAmQAAmQQC8iQNGhF90sDpUESIAESIAESIAESIAESIAESIAEehMBig696W5xrCRAAiRAAiRAAiRAAiRAAiRAAiTQiwhQdOhFN4tDJQESIAESIAESIAESIAESIAESIIHeRICiQ2+6WxwrCZAACZAACZAACZAACZAACZAACfQiAhQdetHN4lBJgARIgARIgARIgARIgARIgARIoDcRcPSmwXKsJEACJEACJEACJEACvZ/AqoKajS4iIyECLmfYRvXtVRRV1mN5bjVKqxqQHBuBIRlRSJFlbyheH7C+pBYrc6vQ0OhFWnwERmTFIjQkBLlldRtdwqDUKMiuXllySuuwcG059hybhvCwXnoRvZI8B00CPYNAiE9KzxgKR0ECJEACJEACJEACfx6BT+blo7bB0+KEcS4HBqVGY1B6tBh/LXa12LAfO3F4EjITI1vsD97YIMblwrUVWLyhEmHS8eh+sRg7IN4YmsFt/wrbEy/+fKPLPOPgIThzv8Eb1bdWkStG7JXPzcfSNRUtdh+370BcdMiwFnU9cePn5aW4RsZfUeVuMbyHp+8An/y78NG5Lep14+lLd5JnJm6j+p5eocLQwdd/Z4Y5cWwK/nXGdj19yBwfCZDAFiZAT4ctDJTdkQAJkAAJkAAJ9A4CNzy3oM2BpiRG4J8njcV2gxM2alNYUQ/7sdOm9MdVh4/YqJ1W6BvsW19fjE9/ym11//TDhuOkPQe0uq8vV2akuAwbvcaSsnpzqZ19C+b2+HDCvT+hqtpvsIc7QpEY50RNvceIRT2d28q8akx/5NfAMKMiHYiJcqCqphH9hUuZXFeSeH1oaZTnxxImtvZ7QhXOpt0yy4zrhSsmYmR2jFnv6EduSbPXxtr86o6acz8JkEAfJEDRoQ/eVF4SCZAACZAACZBA5wnERIcjXj5aNjS5/ReV1mO6vG1+76bdkBTjbNHZlwsKW2x/83tBq6KDR/znj7rzR+QV1Zr2alyOGRIPNR5/W1YGtxiUj76zDHuMTsZg8azYnPLdomLxoqjAdoMSsNOwxM3p6k859t3rdg2cZ8pVX6FOBIPOlle+XRcQHM4+dChO22dQZw/tEe3u/t9SMw71eHnw3PHYeXjL+6VeMx/dtLtps7qwBsfc/kOPGLddFKptaOz0mLYdGI+/7zMAv6+qwPSDhnT6ODYkARLoOwQoOvSde8krIQESIAESIAES2AQCt588FpNGJJkj9W35i1+vwTMzVhlR4NXv1uO8v7U0lL74zS867L9zpvFgUIFCvR9S41rGEnjt+/UBweFvE7Nw/TGj4Gias6Eu5+c9Ng/TDxm62YKDDvzd2Tn4Zl4Bpu7Rr1eIDptwmwKHzF5cbNaz06J6neCgA1+wosyM//92z95IcDA7+uCPSw4d3gevipdEAiTQWQIUHTpLiu1IgARIgARIgAT6PIGoiDCcse9gvPjpGiM6LJUYDPbSKN4Lvy8rNVWHTczEPJmbXyDu41/ML8Qxu/ULNNUggU98sMJs98+Ixs3Hjg7s0xUNdvj6VRNb1PXEjT/WVeCNWRvw85IS4/5f3xQDwyWxLyZvl4obj255Xfpm/l55k79E4ldUy3SB1KRI7DomBZccOgxOmQaxJcqGJs+R7YZuPPVlU/s/8YE5qK71v70PFWHo1Ssn4oOfc/HW9xuwYl2luY5LZArNnnIt9vL1wiI8PXM11kgwSI9M+8gSIeRIEROO3rX5WbC3V1FLPVy07DBky43fOkdXxzNLBJznPl+DVTJ+vV867WV78ZQ5dvd+GJ7lnz5xzYsLsUSeA2vceq5rnl+wUdDPt67ZJRDocm1RDS5+8jdrWIHl33bKwFltxO1Qz6DHP1mJz+cWGLEuWqacjOgfi6uOGIkBKVGBPnTllId+RoVMQ7ldpkC9NycX38nvX7FM09HxXzR1OKYE3acWB3ODBEjgTydA0eFPR84TkgAJkAAJkAAJ9GQC6vaeLHPqdVpEWFCkfQ0AqMaRttF4D7tKYLx3vlmPz38raCE65JbWBqYMnNtLXcrnyLWeb4s9YL9nGk+hrt5vPFv1P4gwcdkT8wwfq04Zvv31OugUlHeu222LZC6wYhwkSxyHLVVUWND7ahU1xu94ZZG1aZ6Fq576DR/csnvAo0UN5Gc/WhVooytrcqpw3+tLzFSC247fpsU+3SitbgjUpQR5xgR2bOJKV8dz8+uLMGNWTouz6fQi/WgMkucu2xnDMqON4GBNO7IaW3E4rO3gZU2dJzBVyb5vbYF/qpG9TtcV/Yn3z8GK9c0in97nnxeV4O///BEPTx+PCUObp6FoAFG9X0/IPfhhflGgOx3nlXKfHr9wAsZ3g6gTOBFXSIAEukSAokOXcLExCZAACZAACZBAXyewQgL9WXEYJjZNu7Cu2YrnMHpwvJkqMWUbv+iwYLnEaJA33VY6wLWFzcbVNv23fMaBfEmpqBkcrFJS6Tdm86Ru3iq/+77uS0uIRFYHmTWsPoKXd725JFB1yoGDsZMYfXEuf+yLilo3km1GsxqNt/z3D2MIamDHaZP7mTf574u3wHfzCqFTUF6SaSun7j0o0GdnV9Q7QPuxNAHL2+L3leX4r0x/sUpKbDj22y7dbGqwxso6v+eCtb+1pcbS0Iwlz4qB7fZ4cboYvlruF2+NseJJcYm8Nf99TTkeessfh+Hjufk4ccoAaApIS3DQoKPH7TUQcfJm/r9frTOG80x5+368BBjVLCXq/aGCjJbC8uZ7NlOEqiUiUlhlG2m73aB4a7NLy66MRztWUcUSHDRo5fkSG0NTjs5aUozXPl9rvBo0dsYNR4/CfaePQ7XcgwIZ+zVPzzfjulKmCo3Mjm0xRns6z0Fp0XjyogmB/f94YYF5BgIVQSvvzckJCA7jRybhlH0GQn8P//X2UvNM3frqIrxz7a5BR8EIDnqfjtg1G6sKqvHCJ6tNmyc+WYXHJV4GCwmQQM8gQNGhZ9wHjoIESIAESIAESGArEXhj1noxCv1xAn4TQ3bRqvLASCaNTA6s68o3v/vjOUzeNtXU79gUtFHfuv6yojQQG2JdcU3guHQx/K2i7u8eb0sPgb23TbN2d3p5lxjF3zfFlrAfNHtBEfRjlYPFGFPDcVNKaZOQodNDztl/SMB1vrW+5iwvCWShuPmkMdhnnP+a9hybirMe/VUCZ5Zi5q8FmyQ6rJSMB/e/0SyAWOfXPvVjFXWtt0SH0x/8GTWdEB1uO3WsOSY4E4OKJG9cvYsRkTRN5WPvLjeGuCX0vPbdOuu0eOnynZEY7fe6OGiHDEy58ivTduZv+UZ0+EyWT32wMtDeWlEPEHvReBybKjp0ZTx6zsc/9ntoqECk1xkTGWaGorFNdhAPng0yZej4yf1NnRXkdH2JX3DSyqHyTLSXvjPSGdoi84srQk0Of5YS02nQj1dErNGiHkSPnTPepKvVseRIxow3ZV+uiHhLNlRtlDEjLiYcT1/QLG7MW1GO38VDZw2zZAQR5iYJbF0CFB22Ln+enQRIgARIgARIYCsT0DfxwUWNn1tPGYsBya7ALjU4LbfyyZJxQovGKRguRukyiWGgcR2sgJQup9+I0za1EgchWmJFSNIK4/qtdfYy+8F97JudWlfXfM26YZVaiUdgTQ+w12fIW/hNLQfI/Pu3xOBbJ2+c977ma0ySefITRyZiD/HuSA7K6LFa3NqtoiLLYpub/DBJrajiQL7NM8Nq25llikyj2G+nzEDTL37JM9eqgSS3kcwIVumf2izu6PQYNDt8WE02WsY2eW4E75i8fVrAa0X3PXPpTqgWESO1KZXlmqbrzUx1yXXVm4/Vx2C5XnX/X9fk7TKmf3xg/NX1bsz63S8KbTciEWnxzWPecTNiVHRlPDpOvada9pmQERAcTIX8UKHozy75xX7PoJHi6SG/eoEyaUSyER20YnVh9Uaiw+7jWo51+6HxRnSok+kdLCRAAj2HAEWHnnMvOBISIAESIAESIIGtQGCgBMzLTvEbf7MXFBuDdsr49MDbemtIKipY5bkv1lqrKJSpDlq+Fnf5f0jQOy39bWJFjhhUVlA+dWX3ildEo0zF0LgIm1r0PNa5tI8rnpsfyF5hr9/U/vW408TF/Q8xntXzQ70G1NjXzx2yb58dM3Dl4cOR0PSGX99IW8WaomBtW0sVRjalZIiIYY+PsJfcBx3PPuPTMP3Aoa12+ebVk1qt72zloPSWgQtHNAVVtI7PK/G/tdc38Cff+5NV3WJZJjEJtOwi0wX0o2WDcJrWJDqcK1lRtlTcga6MR8UvywskM2nTRSlzQVvgh47HSpm67aCWU5HG2bYtLxP7KYdn+oNdWnUh9jkeViWXJEACW50ARYetfgs4ABIgARIgARIgga1J4NLDhgc8FG5/czHe+26DMa4Lpw4LBA3U8X0hooJVPpYUlcGlrKLBzPXXGArZNtHh8/kFRnRQe+ijm3Y3hxnj85ZZwV30qG3NsPHcRTsiT0SVz2Vayfd/FMlbZIldIdkXPv85D0mxTlwuMQ+02KeQbDc8EZoBIrgk2DwzgvfptooxPaVY0yXaGk9qghOrNvinA4yT622t7CyeDFuytIenK+PR5zBSPG/U0M+RaRSbUlQ021JFx6PTPPS5WhU0LcK+nRjkXbOlzs9+SIAEup8ARYfuZ8wzkAAJkAAJkAAJ9BICZ+0/2IgOOtyHP1yBW471ZyDQIJFWrAc1qrMlfoC9fDI713hIfClv4XUufKpMf9AAgyaA4sw1UjcAsRKwsDcW9TTQa9KPcpj++FwzXeJ9SaVpiQ72N84TRyXh9H0GdfpSneGhxgAubooh0ekDu7FhWAfZPdVz5aeFfq+Y6ZKdZFvbNI8tOSwr1oL2WVzZdkyEro4nXUQxzbTx9bwC1EwbCU0V21GJt01F+V4CUVrxTDo6rjP79XdFvUZU1LKXH5f6A3Bq3aCg3zl7O66TAAn0bAIdfKX27MFzdCRAAiRAAiRAAiSwJQmoWLC3zHPXokKCZonQMluMHytmwp0nj8WNx4xu8Zkw2u8+b/eGuGzaCHOsvsE97LZZJmNAXYM/iOQWfFFszmF5lVtLU7mZPzQuQ2FFvYlFYXWlsQOWSPwKLR7bRWwvwQc1qJ+Wpz9cif98thpLJfCfVbSfBuHQWkltCrT56Zw8aNYJLcp9QdN5Wjtma9cdumNzjInLn/4db/6wwXiE6LjUI6G1qQCbMubkmObpD89JVomSqgbTv2Z2WFvUHEejq+M5XAKMalFvh1Mk6OYf6/wpKLVO75Pdw0DrtKhoph4JWt77IQcaKFMzi2gpk1SgKkhtatlvB3/WER3PkzNXmTFo4MjXvlxnuoyKdGCbAc3xOzb1PDyOBEhg6xDonZL71mHFs5IACZAACZAACfwFCJwrb641doGWh8WA1ngCOkVCi76RTWrFzXvymFTz5nvBijJjMGmASc1KoQKG9qXxGy55fJ7pw3IlNxtb6McNx2yDKpnqYKW03BLdnibpIy2hRQNrWutW35oW0yqaKvSfp2yL8x/51bR76oMVkrFhhclGYB33wDnbY9dRLbOB6PEHSsDKR0TgUIPz2Dt/tLo0rD+80T8dJVDZDSufzsvH9c8taNHzXa8uhn70Xn13714t9umGZnQ4VdKIatpMnVZzz2uLcY/U2zl9d9/eLYJRbtRJJypURJogniO/LC7BktXlOPC6bwNHHbRLlhG+tKKr4/n7bv3wrggHqzZUYk1uFU69b06gX2vl49v3CGTlsOqmTelnUmrq83zdsy2ZPXTe+MA0paPvnm08KazjrKWmEtWPFs028u51/jSYp4lnzJtfrzexJlS00o+9XCjxQ6x0tPZ6rpMACfQOAvR06B33iaMkARIgARIgARLoJgKOoPgDmrFi4tgUczY1kIrl7fKPf/hTau7WRmT/PbZpNqZ/XdnsIn7HiWOgaRnTkpqzFKjngxY1UIf2izXrm/tD3fB1GkRn3OQ7cy59222JBdrevq5vnY/bdyAuOdQfz8HqbydJH/ratbtgrC0Lg/24nDayVxwjBrBmi+hqUX5borT3fr69OBPnHDAEKqTY7639enObvGTaGmNrcS9aa3u9eNVo2tLgYj+X7uvKeFTMeFEycpz8t8EB74Xg/gvKGoKrcMFBw3DO/w2FBkQNLvZgoo1teLXYj2n0NHu+aLaXN66d1OLZ0bb6rN0iWWQOn5hlP7TD9VARwVhIgAR6DoEQn5SeMxyOhARIgARIgARIgAT6JgH9iyuntBYNbq8Jwhgf5Z+O0FOvVqcJaBDJqqZ0nNEibCSJu789zkBbY9drLSiXFKOSwSFCvAUyJLhmR4KIphZdV1QLFYGSJU1mT+djv/ZGgbVespTUy/QZl3DKFAFoS7+Z1ykMeZKeM0r612lA9rSs9rHoelfHUyn3WINKqhih91kDgwaLccHnWCvXWy3HhUsAjKTY8FY9gIKP6cy2TtNYW1hj+uwooGdn+mMbEiCBrU+AosPWvwccAQmQAAmQAAmQAAmQAAmQAAmQAAn0SQKcXtEnbysvigRIgARIgARIgARIgARIgARIgAS2PgGKDlv/HnAEJEACJEACJEACJEACJEACJEACJNAnCVB06JO3lRdFAiRAAiRAAiRAAiRAAiRAAiRAAlufAEWHrX8POAISIAESIAESIAESIAESIAESIAES6JMEKDr0ydvKiyIBEiABEiABEiABEiABEiABEiCBrU+AosPWvwccAQmQAAmQAAmQAAmQAAmQAAmQAAn0SQIUHfrkbeVFkQAJkAAJkAAJkAAJkAAJkAAJkMDWJ+DY+kPgCEiABEiABEhg8wnk5OTg22+/RWFhIc4//3zT4dy5c/Hyyy+jvr4eO++8M0488URTX1JSgn/961+mbWZmJq677rrAAO69914sW7YM22yzDS666KJA/eOPP47FixdjzJgxOPPMMwP1L774IvLy8tCvXz9MnjwZ2dnZZt/bb7+NxsZGREZGIiEhwezTHfPmzTPnLSsrQ3JyMvbee2/TvqamBjpe6xyTJk0y9fpD65cvX46RI0di3LhxgXptu3btWgwZMgTDhg0L1K9ZsyYwprCwMGRkZJh9BQUF0POmpKSY7aSkJLOsrKxESEgIdAwxMTGIiooy9Q0NDQgNDYXH40F4eLhZNzv4gwRIgAS6iYDP54N+9LvHKm6323yPW99DutRSVVWF6upq852lx8TFxZl6/Y7XT2JiIrxeL1JTU029fldrO+3P4XAgKyvL1K9fv958z61bt87U6XeqVZYuXYqVK1di0KBBGDVqlFWNBQsWmP8rtK1+YmNjzb5ffvkFq1evRv/+/aH/v+hSy6xZs1BRUWG+YyMiIrDTTjsF6mtra833r/5/sd9++5n6oqIizJkzB3r+oUOH4pBDDjH1+uN///uf2bfDDjvgyCOPDNS/+uqr2LBhQ+A7f8899wz8n/TCCy+guLjYMNp1110xevRoc9z7779vxqu89f83PUaL/r/z1VdfGcYDBw7ESSedZOr1/4tHHnkEykr/v7v22mtNvf644YYboP/P6P8xt912W6D+4YcfxqJFi8x92nbbbbFw4ULDQv9fPu200wLtuNJ3CVB06Lv3lldGAiRAAn8pAhdffLEx7vfZZ5/Adesfl8cccwxKS0sDfxDqTq2fMmWK+WNu8ODBgfa6MmLECPPHkPWHorUzPj7eGN76R6G91NXVmT96y8vLjeFu7VNDXf9Q1T9s7QKC7p89ezb0Dzfrjz6t0zF9+OGHyM/PNwKD/Zjnn38eep5vvvkG+sebVf7zn/9Az6t/VD/99NNWNV5//XX8/vvv5o/oBx98MFA/c+ZMs0//uLz++uthiQ4qhNx5552m/VlnnYVp06aZY1TA0T809Q9z/UPxyiuvDPR14IEHYsCAAeaP92eeeSZQf8UVVxjeubm5eOONNwICxksvvWTOrSLI5Zdfjt12280co9f02GOPmes/5ZRToP1q0THdd999pv+pU6fi2GOPNfX6B7WO3RKSLGFIeZ933nmGhYo899xzj2mvPy644ALTj9PpNGKTtePmm282f2wrh2uuuSYgxrz11lv4/vvvzb3bf//9se+++5pD1JB48sknjTGj1/6Pf/zD6sqMSXmpcXPrrbcG6m+66SaoQaPP04033hiof+CBB6DikP5xroy1Py3vvvsufvvtNyiniRMnBs6topPyVENKRaTLLrss0Nddd92FVatWGcPHfo+UwYoVK4zBovfFKlqv16LPvvKzDDi9Nh2rGk9q+Gy//fbmEH1eVdBTw0iNq+OPP97Uq9Hx2muvGUNGjasLL7zQ1KvYZl2fGiv2c6uop9eiRpS9Xsekz356errp3zIGP/roI/MsKA81UCyD6McffzQGkQpjKhBahpcyVaFROakQqN8LVlEjSEU6ZW0XGm+55RZjQOk90nuqv7Na9PdOf49UiDvuuOOM6Kf177zzjvkd1nb6vKoBp+WHH36AGnA6Jh3r0UcfberVCNPrVoNPmdvPrb8Lajir0Hf//feb9vrjjjvuMAKkfu/oc6MCpZannnoK3333nRnTJZdcEhAbdUwffPCBMdRPOOGEgMiphqMaiPr7os+x/o5p0Xt5zjnnmHuv59DfNatovX4f6XfLK6+8EjD+7777bmNoq5Gv47C+P3T9008/NdetbVQc1TJjxozA95WOVX+XtCxZssQIw3pN+j2gv59W0TbKSBk+++yzVjX0Hul3it4/vU6rqBisvy/6HW9vr/dBf5e06O+gJTr88ccfeOKJJ0y93lP9btGioqt+r+h16/Ok3wdW0WdTnz/9ntXfEavo9Wl/KnTYDWx9NpW7Ch72a9PfLf1d0v9DrPNqX3pu/b5RIWTs2LFW94a7XpsKJcrDLjroc67PuDKxF+Wg4rHeIy06ZnvR69Dfd71Oq+j/BzpW/X4MLnovVCjR71Sr6HjHjx8Pl8v1/+ydB1yV1RvHfwqIyBAQUNzi3nukVs6sHJmm/7TcI0uztFxZmSMtc6ep5cq9cqSZe+89UXHiAlEQlaGg9j+/Q5cA2YLc8ZzP53Lf97xnfs8F7nnOMzQrQz7f+f/31KlTKF++fMxsLWDh3wL+neDfJP6POXr0aKz/maxAdjH/98VqRG5MmkAm9aH771Nn0lORwQsBISAEhIAQEAIviwA3lnzxC2vML6v8sstNlL29vf6izi+mTPwyzC/J/PLKrx4GbQvDySPb4hdiw2aTmwuWo2YGN0UF1SkjE7+4UwDDTTM3jxQSMbE+NVT4hZYbY24cDIkbR54WUhvEsJHmM37ppbYI2+CXa8PGjhsD9sHTSCbDl2Bu7KlRw3eOp3bt2vo5f1B4wn4onKFAy5C2bdumN0XUUDFotfDZ5s2b9UaXG2Nu0gxChw0bNui5cENLHgaBBzfqfFHoQkYG4Qzb4uaHmxP20bhxY2bpxM0Z58jT0JgbFuZzY8ANzptvvhktdOBGjWvEzSQ34AZW5EGBANeVG/BmzZrp9il04OaKTDgmQz7XgsICngSzj6ZNm/47IuhNuWFMMfM5Jq4puXJDY9AYWr9+ffQJNzfmBqEDx8SxcvPMfM6DiZ8bnqjyc0K2jRo1iu6bG2MKs9i+4TSZD7kWXDtulLh2BqED146CAn5WKegxnHLv2LFDj4mbR5Y1CB24YaLwi5wouDHkU+hAASQ/n/ysUZhkSDzJ5iacAsjKlSsbsvU4mc/PBtfC8NnkupE3+bB/g4YTN78ULBh+3wxjZZ/cJPLUnBtvCnuY+Pnm7x43rWzL8PnjM37GOGYKgPji55CJeWROHvy9NwgdmGfYmDLfoG1ADvwdZh7HEFODivf8nHADbODNPliezyRZJgFqI/L3IKaw0DJJmN+sRehgfmsqMxICQkAIWAQBno7z5CTmJssiJi6TFAJCQAgIASFghgQotKNWC7XQJJkXARE6mNd6ymyEgBAQAhZBgCeT9KVAVVfDabRFTFwmKQSEgBAQAkJACAgBEyOQ2cTGK8MVAkJACAgBIaDV27t06SICB/ksCAEhIASEgBAQAkLAyAmIpoORL5AMTwgIASEgBISAEBACQkAICAEhYEkEqM1InzD0wSLJ9AmIpoPpr6HMQAgIASEgBISAEBACQkAICAEhYDYE6LCXEWQkmQcBETqYxzrKLISAEBACFkOAX0QM4cAsZtIyUSEgBISAEBACFkSAEXfihvy0oOmb3VRF6GB2SyoTEgJCQAiYN4Hx48fr8IfmPUuZnRAQAkJACAgByyVQUIXPnTRpkuUCMLOZi9DBzBZUpiMEhIAQMHcCwcHBCA8PN/dpyvyEgBAQAkJACFgsAVdXV4uduzlOXIQO5riqMichIASEgJkTuH79upnPUKYnBISAEBACQsCyCRw7dkxHq7JsCuYxexE6mMc6yiyEgBAQAhZDwNHRER4eHhYzX5moEBACQkAICAFLJDBmzBhYW1tb4tTNbs4idDC7JZUJCQEhIATMm4CtrS2cnJzMe5IyOyEgBISAEBACFk7g0aNHCAwMtHAK5jH9TP+oZB5TkVkIASEgBISAJRAwRK6wt7e3hOnKHIWAEBACQkAIWCSBBQsWoGHDhqLdaAarL0IHM1hEmYIQEAJCQAgIASEgBISAEBACQkAICAFjJCDmFca4KjImISAEhIAQEAJCQAgIASEgBISAEBACZkBAhA5msIgyBSEgBISAJRFYsmQJDh8+bElTlrkKASEgBISAELA4Ardv34bBpNLiJm9mExahg5ktqExHCAgBIWDuBPbu3Yvw8HBzn6bMTwgIASEgBISARRNo3749rKysLJqBuUxehA7mspIyDyEgBISAhRCwsbFB1qxZLWS2Mk0hIASEgBAQApZJgOGxMzrmQeTTfxDx5JllLkAazlqEDmkIU5oSAkJACAiB9CfQpEmT9O9EehACQkAICAEhIAQylECZMmXAMNkZmTqMP4T3Rx/IyCGYRd8idDCLZZRJCAEhIAQsh0CdOnVQtWpVy5mwzFQICAEhIASEwEskQBPGP//8M1rL4OHDh1izZs1LHEFUVwMGDEDmzM9vV7dt24ZmzZqhY8eOWLt2LW7cuJEuY7sWGI5LNx7i2T//pEv7ltSotSVNVuYqBISAEBACQkAICAEhIASEgBAQAgkTsLOzw7x58+Dg4IB69eph7ty5CAoKQtOmTROu9BKf1K1bFwEBATh27BjGjRsHZ2dneHp64vXXXwfHnlbjXLjjmp6Vk32Wlzg78+wqk7KTEdGNea6tzEoICAEhYJYEvL29kSlTJpQsWdIs5yeTEgJCQAgIASGQ0QQOHjyIESNGYNGiRXj11Vexfv160MfCihUrsGDBApQtWxa9evWCm5ub3vxTMHHq1ClYW1tr7QO+v4xErYx9+/Zh4cKFCAkJwePHj+Hl5YWWLVuiZs2aqR4CtRxaDd+r61cp6YopH1VMsq1nald9/uZDHL0cjDL5nVC+YPYk67BAeMRTZLWxUt9tEi8eHBqhNT+c7F4O28RHk7KnInRIGS8pLQSEgBAQAhlMoF+/fmjVqhWqVauWwSOR7oWAEBACQkAImC8BRo+gkL906dLo378/bt26hf/973+YNWsW/vjjD61h0KNHD3To0AFFixbFxx9/rDfFLi4uKYLCsJg8B4+MjISfnx+cnJzw4MED9OzZE59++ikKFCiAiIiIWK8nT57oe9a5fPkycubMieDgYNy8eVMLQfz9/XXezp07UzQWFqbw4MOxB3H73iM4ZrNG/pz2mNS1PPzUvbO9DawyZ8LP6y6hoHs2tHwlDwLuP8aoP87hwOlAPGVllQrndcTCL6O+p2w6cRtPlEPKtyrl0s9i/th6KgCDZp7C3H7V4epog3GrL+L4pXsokscBA1oWR15XOzwIf4L24w7C705U5K58uewxumNZeKl3U0mmJyYxFbIyTiEgBISAEEgXAjzJ4JcJSUJACAgBISAEhED6EejTpw+aN2+OCRMm6E4OHToECgiGDx+OwMBAreVAoUOnTp0wcOBAbNmyRQsg6GshoXTx4kUtTKCQgYKDZ8+ewd7eHo8ePdICCwo56MeBz+7evYupU6ciW7Zs2qEkBQx8zrp0MPn06VN9TwEFhQ7Xr1/X46K2g6OjY0JDSDJ/4toL2pfDr59Vxk8rL8DGOkoFoceUo6hRKgdu3AnD4bNBup2qxVwxe8tV7D15V5XLjKEdSqNKERc42dlE9/P17NNwUMKKuEKHW0qIwWfFCjjBztYK732/TwlenqFCMRccUe33nXESS/tXx9hVF7TAoVF1T+R3t8P6w7fR5of9WDWkFjxdTCOalwgdoj8OciEEhIAQEAKmQKBcuXLIlev50wJTGLuMUQgIASEgBISAqRAoWLAgcuTIAYPmQuXKlZElSxaMGjVKayNYWVnpqdDB8/79+3Hu3Dk0atQIb7/9tjbFiG+e/P/9448/6sMDmkEwDDaFDDxMyJs3rxYi8P7q1ata4ECfDRQ08MCBvhv4jPcULFAYsWPHDq11ceXKFS2IePPNN7Xgo1SpUvF1n2Te9jN3sXjLNXRr4oWS+ZzwMCxSayncCApXApJ/sGrnDd1Gs9p58Ofumzhy8R66NSyEM1cewNcvBOOUkKJfy2KoV9Yjui9qRuRRWhHX7oZhwc7rqnxB2GWxRpeJh5UPCmtM/qgCPppyDI8eP0XbBgVQKp8jzl59oPtjI7tP30GZws4Y1iZqTl0bFMJFv1DkcDQdXxMidIj+OMiFEBACQkAImAIBqm9KEgJCQAgIASEgBNKXADf3MROFAu3atcP777+vs7t16waaYNDkkUIDakEwrLWrq2vMarGu6ZyyQoUKsfJ4QxOKmInCiRo1akRnxW2TESx+/fVX3LlzB8WLF9faFxUrVnyhEJv04/DVzJO6zwWbr+G3tZej+99x+i4ehEbq+7dr5sbg90rgjBIMnPS9j3er58bSAdWx0/suJv15UZtL5HKzw6DWJVBDaUIwVS7ijK/mnsGFaw+QPZsN9p8Lwv0HEZj9RTVcvxuOK8oXRKE8yiRjs68uT62JL1sU09chql9PZWYRMxXxNB3TCo5bfDrEXD25FgJCQAgIASEgBISAEBACQkAICIEECdC8gZoHFCBQMEFTCAocaNJATYT0ThRwUOBBQcWwYcMSFXIkdywUOHQad0hpUDyFl/LHUMEru3YEOXPjVdwLicBfQ2rjlT5bQGHCysE1oZQX8MOK89jnHYjVX9fUGgwNy3vAI7st9vsEYcwKH1z3D8XkXpXw5W8nlCmI8lnx5Fms4YzqUlZrRMxTUTImKw2JdSNehZ1yKElnlMWVACKbMrlgqv75FrxS1g0TupSPVd+UbkTTwZRWS8YqBISAEBAC2tEUv/BQxVOSEBACQkAICAEh8HIJUNAQ02cCI1Vkz568SA1pMVJqQaxZsyYtmtJt0PcjBQ5Mi7+qoZ036hv1Y8muG9q/A3U+qOHQvFpuLXDg88aVc2Ht3lu8xNzNVzFJCRqqKp8PeZVgIp+HnRY6bD4ZgHJKy+HgmUC0eD0fCivnj9P/uoRPmhaJNsEooEwvmIYvOYtxncujopezvue4bgc/QlYlfDA4qNQPTPCHaDqY4KLJkIWAEBAClkyA0Svee+89VK9e3ZIxyNyFgBAQAkJACJgtAUahGD16NCZOnJjucwxT2g2MRtHzrcJwyBqlXWDo9IiKJEE/DGM7llNaHYbc/94pGKDWw50Hj/HL35exV/mEeBgSqX01VFYOIb9sXky1aY2bSpOiaG6H/yrGueo35xR2Hg/QAobaZd3hr5xMnvd9oLUjfupWHoWVOUWeOCYWcZow6lsROhj18sjghIAQEAJCIC6Brl27onbt2kjMO3bcOnIvBISAEBACQkAImA4BOqZkaE76bbCUxPCZG48F4MTlYORSAgaaeLxbIzfyu0VpQpgyBzGvMOXVk7ELASEgBCyQgJubG0qWLGmBM5cpCwEhIASEgBBIWwLjx48HQ2MaW2IITEaosKTEiBcxo16Y09xF08GcVlPmIgSEgBAQAkJACAgBISAEhIAQSIRAeHg4Zs6ciTlz5iAwMBC//fYbGjRokEiNjHlE/01xI2hkzEik1xclIJoOL0pQ6gsBISAEhIAQEAJCQAgIASEgBEyAwJgxY7Bu3TqEhYXh4cOHePXVV41S4ECUInAwgQ9UMocomg7JBCXFhIAQEAJCQAgIASEgBISAEBACpkhg+vTpWLlyJTw8PJAvXz5s27ZNR4NiFAhGg5AkBNKTQPoHUk3P0UvbQkAICAEhYHEE+vfvDzqYkiQEhIAQEAJCQAgkTODZs2cYN24cWrZsiSVLlqBTp06oW7cuduzYgaJFi6JIkSJGK3Dw9vbG999/n/Dk5IlJERChg0ktlwxWCAgBISAEHjx4AH4ZkSQEhIAQEAJCQAgkTIDCht27d6NGjRpYu3YtfH19dUSIoUOH4saNG/jss88SrpzBTy5cuAC+JJkHAfHpYB7rKLMQAkJACFgMgezZs0v0CotZbZmoEBACQkAIpJbAzz//jLx58+rqy5Ytw4oVK3S46YCAAG1mQWGEsaaCBQvCycnJWIcn40ohAfHpkEJgUlwICAEhIASEgBAQAkJACAgBIWAqBFatWoVRo0bh66+/RtOmTdG5c2e4uLhg7NixRjuF0NBQPH36VAQPRrtCKRuYaDqkjJeUFgJCQAgIASEgBISAEBACQkAImAQBOpBcsGABBgwYoAUOW7ZswbVr1/S9MU/A3t7emIcnY0shAfHpkEJgUlwICAEhIASEgBAQAkJACAgBIWDsBDZu3IjZs2drYUOLFi30cDdt2oTSpUujePHixj58GZ8ZERChgxktpkxFCAgBIWAJBHhas2/fPkuYqsxRCAgBISAEhECKCWzfvh180WFkr1690K9fP93GnTt3cOLECfTs2TPFbb7sCoxS9dtvv73sbqW/dCIg5hXpBFaaFQJCQAgIgfQhYIhe8corr6RPB9KqEBACQkAICAETJbBu3Trtv4HD79KlCz788MPombi7u+Pvv/+OvjfmC39/f5w8edKYhyhjSwEBETqkAJYUFQJCQAgIgYwnwOgVpUqVyviByAiEgBAQAkJACBgRgYMHD2LSpEmoVq2aHhUdRppqooDE0dHRVIcv445DQKJXxAEit0JACAgBISAEhIAQEAJCQAgIAVMicOzYMfTt2xctW7bUJhWmNPb4xurt7Y1nz56hTJky8T2WPBMjIEIHE1swGa4QEAJCQAgIASEgBISAEBACQsBA4PTp0/j888/RuHFj9OnTx5At70LAaAiII0mjWQoZiBAQAkJACAgBISAEhIAQEAJCIPkE6KOBziIbNWokAofkY5OSL5mACB1eMnDpTggIASEgBF6MwJgxY7B3794Xa0RqCwEhIASEgBAwAgIXLlzA5cuXUzWS8+fPY+LEiShXrlx0hIpUNWSElXbt2oU9e/YY4chkSKkhIEKH1FCTOkJACAgBIZBhBC5duoRr165lWP/SsRAQAkJACAiBtCAwc+ZM9BswCJ27foQhQ4aA0ZmSmyhwoCkFIznReaS5pUePHmHbtm3mNi2LnY8IHSx26WXiQkAICAHTJODs7Iz8+fOb5uBl1EJACAgBISAEFIHvvvsO8xYvR4nX30PrL8dh2/5jGDpsRLLYbNy4EZ988gkqV66shRXJqmRihUJDQ/H48WMTG7UMNyEC4kgyITKSLwSEgBAQAkZJgJoOmTJlgpeXl1GOTwYlBISAEBACQiAxAtRQ2HfkBHqPXYpsjs7RRcd/2hR1a1XXAonozDgXNMf48ssvwZCSM2bMiPPUfG43bNgAf39/dOjQwXwmZcEzEaGDBS++TF0ICAEhIASEgBAQAkJACAiBl0fgm2++wY6DJ9Htu+lwdHGL1XHYg2DMG9EdbVu9i3bt2sV6xpuLFy9q3w0Uuo8ePRpWVlbPlZEMIWCMBMS8whhXRcYkBISAEBACQkAICAEhIASEgFkRGD9hArbtO4L2A8Y9J3DgRLM5OaNW826YOPkX0GlyzET/BtRwoHnh999/LwKHmHDk2ugJWBv9CGWAQkAICAEhIARiENi0aROyZcuGWrVqxciVSyEgBF4GgX/+AR5HPkPWLHJu9TJ4Sx/mQ2Dq9F+xaPkqtOnzI9xyF0hwYmVqNsT9oDtYvGwaMmfOjL59++roFtOmRd0PGDAAWbNmTbC+uTygU03O38HBwVymZNHzEKGDRS+/TF4ICAEhYHoEeNrTuHFj0xu4jFgImDAB/+BHGDT3DM5fvY+nz/6Bg70N+rYoisaVPY12Vot238BBn0CM71zeaMcoA7MMAnPnL8SM2XPRrMtAFCpdOclJ12rSFo/DwzBv4Wz8oyR9zZs3R758+fDVV1/B1dU1yfrmUGD79u3w8fHR2h3mMB9Ln4MIHSz9EyDzFwJCQAiYGAF+AYuMjDSxUctwhYDpEoh48gxtfjiASPX+yTtF4OmSFTM3XsX2U3eNWuhw4VYIDnkHmS54GblZEJj+20zMnDMP73T/CuVqvpHsOdVr1RVPn0YqwcM8FC5cGD/99JN2opzsBky84OXLlxEQEGDis5DhGwiI0MFAQt6FgBAQAkLAJAgwJrm1tfz7MonFkkGaBYEle24g7NETDO1QGm9WzKXn9HoZd1ipKDJMNLmYtfUqNhy+jYgnT1Gvggc+frMwbKwy4ZTvfYxe4YO2r+fDvK3X4Oxggy4NC6JyYRddN/TxU4xZ5YOjF+7BPqs12tbJhyZVorQnBs8/g2t3wlCzVA7kzG6Lhduvo2yh7Oj5thfcHG0xbcNlbD4WgOxK6+LtKrnwVqVcyGZrhWuB4Rg89zSu+YdqQUm78Yd0X+z7524VkuxXF5AfQiANCAwarJxG7tmPd3t8g5JVX09xiw3f/xjObrkwfOQPOHfuHAYOHJjiNky1QoECBbSWh6mOX8Ydm4CVihH7XewsuRMCQkAICAEhYLwEihcvrh1pGe8IZWRCwLwILNhxDb5qA/9Nm9LIYh3lyyGzEjj8K3PAn4f9MGG5D3K52cHFMQs2HvSHR46sKJnXEWdvhmCJEjYcPH8PxfI74tTF+9h49DY6NiioIQ1ZdFaV90O5Is649zASa/beQtMaueGgBBA3gh7hsE8QfG48xJ7TgahQxEWXtVdChkpezriuhAvK0gN3HzzGn3tuwv/hY9Qt6659TgSovID7jxES9gT1K+dELtesyKvGV+VfYUdi/ZrX6slsMopAl27dcfTkWXz0/e/IW6RUqoeRx6skbl+/gv27tsLWxhrly1uGuVDJkiXBQwZJ5kFAjorMYx1lFkJACAgBISAEhIAQSBcCAcGP4eZiqwQBVjh97QFWHril+8nvng0d6uRXQgR/lFQaCHM+q6LzP5l2DBuUYOHd6rmjx9PlrUL48PX8WHfEH0PnnYGPMn0o6umAXScC0PntQvjoDS+tMfFqv23YcDxAt8u2TysfEjvV/Yw+VVC2QHb0iXiC+2FR5lVsny9qS8zf4Yvf11/F161KwENpRXzRrChCHz3Fxnt++jp6IOqCmhmJ9RuzrFwLgdQQ6NCxMyKzOKHvz6uQ2erFt1utew/HzpWzsWrdRtDBYs+ePZM1rFu3buHAgQNo2LChxTlkfPLkCS5cuAAKL+ImRgYpVKgQWrZsGfdRkvdXr17FwYMHkTt3btSoUUM0L5MkFlXgxX8LktmRFBMCQkAICAEhkBYErly5opvhFwZJQkAIpD8Bd7WJ9/UL1R0FhUTgmDKFCFBaCNRsoGDgmNJiYKo/eKd+Dw9/orzOR5le6Az1o0IhZ33p5mSr30OVuQYFD/QTMW+jL5buuKHzeX9IaTew3ZipZD4nfRvTKeQPK85jmxJIBD+IiC5Kh5f53bJF38d3kZJ+46sveYkTOHbsmPJF8BRVqlTB+fPndWjHIkWKJF7pJT7t3Lmz1hZ444034t2QvuhQenzSC3dDI9Ft4E+qqdi/By/S9mvvdsIJ99xYtnA8SpQogfr16yfZ3OLFizFjxgzY2tri7bffTrK8MRUIDQ3VPiwYrSo16d69e1qo4O3t/Vx1fjZz5MjxXH5SGRQ4tGjRAq1bt8bvv/+u3zt06JBUNXmuCIjQQT4GQkAICAEhYFIE+AXqrbfe0qcUJjVwGawQMFECRXLba22DE1eC8VopN/0auvQsTl2+r2dkZ2cNyhgGti4RPUO7LFbR17ywsXo+xKazfdTX0DKFndGqVp7o8nRUGTM5KV8M1nGEGH8d8cPKnTfQu0UxvFLMFVtP38Fvay/FrAY7FdaTQgw6wjSYhbBAcvuN1ZjcJJvAokWLsHnzZn0azAgEDO9oLEKHx48fo2jRoti3bx/Wr1+vQy/TXKFp06bJnl9iBb8ZMhRnL15Fr58Wq2JpJ3Aw9Fm+diPc8DkGRsOoXr16ktoLGzdu1NEfGPWJQgcKhFauXKk1ABiKcujQofrEnvlz587FqVOn9Mn92rVr0a5dO4wcOVK/Pv/8cyxdulSXX7FiBRYsWICyZcuiV69euH37NgYPHqyjSu3evRv29vYYPXo0wsPDMXHiRJw8eRIPHz7E5MmTUbp0acNUYr1TODJv3jzUqVMH1M4YP348+vfvr8fCNoYMGYJWrVqhTJkyiNu/m5ubntO6detw8eJFrX3Qpk0bDBo0CBRcMPIH08KFC/Hs2TN8+OGH+p7CAs6BiZoLU6dO1ZFBPvnkE+24k/27u7uDn2G2x3oFCxYEWWbPnh3Xrl3TL92A/EiSwPP/AZKsIgWEgBAQAkJACGQcgeDgYFy/fj3jBiA9CwELI9Dm1fywUpv+3tOO4499N7VzSL/AR9EUGiifCQ9CIrH7bCCslXDBKZsNcjnHFhxEF45xkVOV8XS3w0mlOeHjFwJHOxtls26F3Mr/whPlrOHo5WDlryECEZHP9DV9NBhS5FNlI6FSLmdbPIpUZhTKnINp3/kghCizCqb65Tz0+7Al57BfaU9sP3NXm1Yk1q+uID9emECWLFmwZcuWWO0sW7ZMnzwzCsP9+1ECq1gFXsINT/y5GeUm94svvsCZM2f0iXW3bt20RgA3qS+S9h08AjtHV9jYJv35T20/jTsPxNlzPiDPhJK/vz/44ny4WedacMNN7suXL8e3336rT/opgGCaMGGCvl+yZAkoNKKzZtY/dOgQ9uzZo0NXUmBjEAj88MMPWpjEtuhniWYK3LSTaUhICI4ePaoFAWfPnsX06dOxatUqXS6+8VJoQSEFhQvUkGEdJmoV3Lx5U19funRJCy7i65/zolCkWbNmuh+ub6lSpXR7FIBQI4EvOzs7LaSZM2cOKlWqhDt37ui2+aN37954//334eXlhbFjx+p8Ou6k4IRjo1CF10wUOJARw3l+9tlnOk9+JE1ANB2SZiQlhIAQEAJCwIgIUK00b968RjQiGYoQMG8CTkqTYVbfqhj4+ymMVht4Q6pS0lVf9mmi/CeEP8W6fbf0i5nNaufB4PdKaA0IQ3m+G5xPGvKmfFxRtXsac/6+gjmIMp0a1aWsdhT58aQjhmLgtcH3AzMbV/bE8t038dWsU7rMBw0LwFeZa4xbdh4FlK+JGkr7oaIy6ahWOgc2HfLTLxZcO6w23JWJR0L91isbJajQjcqPVBPo1KmTPg2vVauWboObxVGjRoEbPp54r169Gu3bt09R+7TPnzZtGrj5ffToEVxcXLTphpWVlX7nJpAn6dwwMy9z5sxg2EX+z2Ce4cVNbr58+XQZqsr7+vrqk26OjZp0PP2mA0OaYaQkcfPr4uQA18LlUlItVWVtHbODPgsSSrt27dKPuOmmIIG8KGBh4oabfg74Ti0EJq4XI2NQOEENgI4dO2qHzYcPH0bdunVx5MgRfcpPIQQFGcOHD0dgYCCoZdCjRw/QBIIaI+XKldMaE2wzf37lw0VpHzRu3FhrkrB9rkHcRC0L+kagNgzLUrOAycnJSW/weU1hBFNC/X/11VcYMGCAnlefPn1Qu3bt6LoUEsRMzs7OWgBhyKNgg34yGjVqpD8/U6ZMMTzS4+FniqYYFNhQcMFEpj/++KMeY3RhuUiUwPMrn2hxeSgEhIAQEAJCIGMJfPrppxk7AOldCFgggRIqEsWqwTVBDYPg0Ai4OGSJNnnIqswYRnxQCsPblsIdFTXCSoXKzKGeM9Ec48CE+tHEqqoIFDHv87jaYV6fqrrdQBV9wt7WWmk8RH09jVkuuoF/LxiOc74ShNCJJE0neN+lQaHoaxajgIMhMsNUGTqfpCYFnWEyJdavLiA/XogAN7Vbt27VavzcxFN9vVq1anpTSvO4TZs2pVjoQCGCq6srIiIi9KkzN4/c6PNl2JTyNPof5SmU98znabaNjY2+Zx5fQUEqIooSUBju+c46jo6O8PPz0+rz3HynVOjA8XXt3AGjx03CqaJlUPaVBi/EMKHKSyZ+DQ+X7HqTnFAZmgAwkTk3+tyE79ixQ5snkEfcRLOG/fv367Cc3HzTFIPC/Z07d6Jv376YNGmS3tTTvIFaLBQgUShA4Y4h0RQhZqIpAjUcyJs+EChM4NrHTYWUfyauB4UoMTVgKleurNeOa3rixAldjXnx9f/RRx+ha9euWvvjm2++0WvI9aSAhJ+BuGOLOYZcuaLCAFNARR8kFMYYUsz5GfL4PmLECG2mEzNPrhMnIEKHxPnIUyEgBISAEBACQkAICIF/CXBzT02B+BI3+YwckZrEdpNjkhG3bXvb/zY9Ma9jlsumyvAVX0ptv/G1JXmxCdAGnjb/FDpQ24Bq62FhYXoDmZBtf+wWYt8VLlxYq7nHzn2xO5oQTdc81AAAQABJREFUHD9+XAtBuLFlRIKqVaumKqoBR8INO0/O5y0Yrzbk1ihVrc6LDTBO7Q3zJyPw6hmMHjU8wdDR3Lxzg89E3xU86ecaUDuBQoOYKRN/aVWiCYbBHKNJkyZauMPNODftXCtuyKkdQkEEfT3QFIGJZikUPnCjz7J8GTQFuN40AaGQiPUSCvVZrFgxVKxYUQtIPDz+0zSigIJ90YSDWhD0sUANjrj9U1uFwhWOg8mgQUNfIozyQaeRnp6eWgMjICBAm9dQS4OJviv+/PNPfPfdd2jbtq02v+C1IRn48N5wTWEWhSizZs3SGhqGsvKeOIFMSrIXZRSXeDl5KgSEgBAQAkLAKAhERkaFy4vvtMYoBiiDEAJCQAhYMAFuDrlxpcp8hQoVtNo+N4L0I8ANJE+S6RMgo8zkKPiYPXu29jtAUwtqOlDQwE0unTNSY+FFE/0TrFy7Ho069EPJKq+9aHO6/upfR+L6yZ16883IG2mZKKigwIDaAcmZP7eP9N1AZ5SGzXjc8bAMzRZo4hGfWUXc8hRO0HcDzSPo5JOJa8PEttiPQfMgbv80H+F3A44/bqKmBAUFHEdiiQzYfkLziVmXZZMzp5h1LP1ahA6W/gmQ+QsBISAETIwAT1QYc/y119Lmi5yJTV+GKwSEgBAwWQIZvVmjD4OPP/4YPF3nCT79FdDpoMFWPy3Bzpw5EzPmLsJb7fugXK1GL9T0orEDEeh7BjN/m4YCBQq8UFvGXJlOJA1Ch/nz52vNmO7duxvzkGVsySQg5hXJBCXFhIAQEAJCwDgI0OaWYb1E6GAc6yGjEAJxCdxTPh+eKN8PCZlhxC2fHvf04/AgPDJVJhvpMR5pM4pARp8O09EhQz/myfNfiNb0WpsuXbrg9OnTWPf7WNjZZ0fRCjVS1dXCMf3hd+kUxo0eadYCB8KhGc6aNWs0pwMHDiTqLDNVMKVShhF4cf2hDBu6dCwEhIAQEAKWSIC2mbRPlSQEhIBxETh+JRgNv96JNwfvwsoDt9J8cMdUCM2ZW64mq12O5Z3v9qD2l9uwWkWvkCQEDARehsDB0Nf48ePRoU0rrJnxPc4d3mXITvb7/NFfwu/qOYweOUybgCS7ookWpGmDwXSSfhroOFKSeRCwUs4yvjOPqcgshIAQEAJCwBII1KtXz+xPeyxhHWWO5kXAR4Wr7DzuELKrqBUjOpRBo4o5VUSJzFiy5wa6TziMcGVTXa1oVIjNmn23wtPNDkVzO6QIwlLV1pz1V9DtTa8k63mqqBjVirvC9244lm+/jtzudiiW+3l77yQbkgLxEmBEAoZgpOO/5PgAiLcRC8mkv4jHYSFYuWgWHJzdkTNf4aRnrnwY/PbdRwh9EIRlC+ageLGiSdcxsxJ0PBkzkoSZTc/ipiOaDha35DJhISAEhIAQEAJCQAikLYHJ6y7BKnMmLOpfAzVL5IBdlqhoEZFPVDjDZ//gjx03oN504n2k4SZthxHdmrUaS0UvZ0z7pBJyKQHH5DWXlDO66Mdy8QIEGO2BkSnatGmjowa8QFMWU7VHjx6o/+or2LroZxzf+Xei8458FI6f+3+ArFlt8ecfS+Du6qwjQCxYsCDRevJQCBgzAfHpYMyrI2MTAkJACAgBISAEhIAJEDjsHYhG1T3hkDX+0JRhj55gy8nbaFg+Z6zZPAx/gh9X+ODEpXtwUaE4uzcqhNolc+gyT5RgYtyfF7Dr5B3kcc8GtzihOkOV34Yxq3xw9MI92Ge1Rts6+dCkimes9pXsAe3qF8BPS87hemAY8rtli/VcbpJPgE7+pk6diq1bt6oNcVbtfPHHH39MfgMWXpLROwrOnYtpv43Fs6dPUKlu03iJLBr/FZzss2LJ7Kmw/vd4ePfu3Zg2bZqO7sB2JAkBUyMgmg6mtmIyXiEgBISAhRNg9IodO3ZYOAWZvhAwHgL3wyK1NkOJvPGbL1AD4vWKHpi37fpzgx625Cw2KZ8Lnjns4OsXgi+mHwfbY1qy+wb+UKYRHi5Z9f3WI/763fBjxNJzWLfvFrxy2yNCaVQMn+8N/+BHhsfR7yXyRI3rZuDzz6ILyUWCBDZv3oy2bduia9euOswknf15eXmhTJkyOpJQghXlwXMEGDq018fdsf73MbjifeS55yt+HgzXrE+wcvHcaIEDC9GscPDgwdizZw8mTJjwXD1zzGBY0zlz5pjj1CxyTiJ0sMhll0kLASEgBEyXwJ07d3T0CtOdgYxcCJgXgcAHEXpCLvY2CU6sfd0COH/1Pm4EhUeXobnDvtN3tYbErz0rYV7/6vrZttN39PsO9V7A0wEzP62MaR9XhFcMoQbr7joRgM5vF8L4zuWxbEAN2Khj4Q3HA6LbN1y4OmbRl3cfPjZkyXsyCGzcuBHNmjXDDz/8oH03vPnmm6hevTrq1KmD4OBgvP7668loRYrEJUABTuv33sWScf1jPVqq7m9dOIHxY0bHyjfckP+IESN0dIcxY8YYss32/fbt2+D/e0nmQUCEDuaxjjILISAEhIDFEHB3d5foFRaz2jJRUyDglt1WDzMwJEpDIb4xl8nvpH0rzN9+LfrxBaXZQJ8PFQpl13n5lbYDtSL2KFMNJu/L91G6oJO+5o+KRZyjr+m4knXnbfRF/cE70UBFzeD9IZ+g6DKGi6CHUUIRN8eocRry5T1hApcuXQJNJxhikpoOjo6OCAwMRMGCBZEjRw5kyZIFH330UcINyJNECXz55ZeoX+dVjO3ZBJsWT8WEXk1h908oJk+aoFknVJlOKSdNmgQKhPr3jy20SKiOqebTSSlfksyDgPh0MI91lFkIASEgBCyGwPDhwy1mrjJRIWAKBJzsrLWwwOfWw0SH27Zufvy88kJ0mYIeUf4VrgaE6bzg0IgoM418UeYQbi628AuK3yTC2T7qK2yZws5oVStPdJue/5piRGeoi/M3o8aVJ0eUmUbMZ3IdP4HChQtjy5Yt+uH06dNx//59FC1aFJ9++ilatGghDiTjx5ai3L59+iCnxwKcO3cWbzWsg1atWmmzlaQaKV26NH7++We9Fp988gl++eWXpKqY5HOGzJRkPgRE6GA+aykzEQJCQAgIASEgBIRAhhCoXNIVG/b7od87xZDNNn5nks2qemLiHz7R48uizCEoNFi95yZyOmfFjlNRqtQNK0Q5m3ytrLsOd8mwmzmVNsW6A37RdVneU4XBPKmcSJb3yo7KXi7ar0Nu19iCBZphLNh2Dc5OWZAvhziRjAaYzItZs2bh5MmTKF68OHr16qVV+x88eABxZphMgIkUo8ZI7969EymR8COuB7UlJk6cqH09fP/99wkXNtEn9BkiyXwIiHmF+aylzEQICAEhIASEgBAQAhlCoOdbhbWWwodjD+L4lWBEPlW7fZUyZVLhI/5NDKPZoGouw61+/7xZEVgr4cMkRrBQAoTmr+UFzSyY3n81H7IrYcG4Zefx1axTKFc4ygxDP1Q/pig/D4WVVsScv6/g0ylHtRPKo5eD9WMKG3xuhqD3jOO4qTQpPm5SWI3FUFPek0NgyJAhOHjwoBY4UMOBa7lixQrwpF1SxhN44403MH78eO3jqHv37ggP/89fSsaPTkYgBGITyPSPSrGz5E4ICAEhIASEgPES4AkbPXnTmZkkISAEjIfAgQtB6D/jJB6pUJbdmniha4NCyR5cUEgEHO1sYGP1vGSAz5zts2ihBhuMW4YCjkDlJNLe1lq1EaXEu+98ED6fekybfXzWshj+VytvssciBYHFixdrAUOtWrW0Gn/mzJlx8eJFfPjhhzpkZrZsojViLJ8THx8frfXAiCJ0NOng4GAsQ3uhcSxZskQLulq3bv1C7Uhl4yAgmg7GsQ4yCiEgBISAEEgmgVu3bml132QWl2JCQAi8JALVi7pix491sOLbmmhc2TNFvbo6ZHlOmGBogM+Uf0n9PK7AgWWYl0uZWxgEDswrmdcBC1REi91j64nAgUBSkJYvX45NmzbhlVdeQc+ePUGBA9POnTt1nggcUgDzJRQtVqwYxo4di6CgIAwcOFC/v4Ru072LGzduSPSKdKf88joQocPLYy09CQEhIASEQBoQyJMnj/aongZNSRNCQAikA4E8rnaIz6FjOnSVYJPUjCjiaa+FFQkWkgfPEVi9ejXWrl2LkiVLan8D1tb/uX/r3LmzVud/rpJkZDgBOvn85ptv4O/vj0GDBmmTi1OnTmX4uF5kAE5OTsiePbZJ1Yu0J3UzloCYV2Qsf+ldCAgBISAEUkggLCxMq1za2UXZfaewuhQXAkJACAiBeAj8/vvvWL9+PSpWrIgvvvgCVlbxOwSNp6pkGQmBCxcuoF+/ftGbdZpb5MuXz0hGl7Jh0GyEXgDoNFOS6RMQoYPpr6HMQAgIASEgBISAEBACQkAIpJrA5s2bMXPmTB0Wk35zYmo4pLpRqZghBCh4MEQXefz4MX766ScwBKokIZCRBETokJH0pW8hIASEgBAQAkJACAgBIZCBBHbs2IGpU6eCIQrpE0AEDhm4GGnUNYVITLNnz9baAjS9oMmMJCGQUQTEp0NGkZd+hYAQEAJCIFUEqAK8bdu2VNWVSkJACAgBIfAfgb179+qIB9yQisDhPy6mftWgQQPw9d1332lHoCNHjoz1f/PatWto2LAhgoOjQswa43wZvWLp0qXGODQZUyoIiNAhFdCkihAQAkJACGQcgTNnzuD+/fsZNwDpWQgIASFgBgQOHTqEKVOm6IgUAwYMEA0HM1jTuFOgg8khQ4aAZhbTp0/H4cOHdZH8+fODUS/Gjx8ft4rR3DN6xYMHD4xmPDKQFyMgQocX4ye1hYAQEAJC4CUTYLg28Wj9kqFLd0JACBgNgStXruDvv//GsWPHUj2m48ePY/jw4ShYsKCOepA1a9ZUtyUVjZsABQ/ff/89PDw8MGHCBOzbt08PuGPHjti9ezcuX75slBNwcHCAhGc1yqVJ1aCslNrNd6mqKZWEgBAQAkJACGQAAYbR4hdkT0/PDOhduhQCQkAIZCyBb78bjtV/bcDRo0d0SNBSpUqlaEBnz57VphT04TBs2DDY2NikqL4UNj0COXLk0E5CKXCghgu1CN59910cOHAA586dQ7169YxuUpkyZYKbmxty585tdGOTAaWcgAgdUs5MaggBISAEhEAGEqCwQQQOGbgA0rUQEAIZRqBz1+64fCsQXuVqwK1AaSyf9yvuB9/TJhLJGdTp06e1hkORIkX06bcIHJJDzfTL+Pr6wsvLC6VLl8aWLVsQGBiotQiqVKmCxYsXg1oFJUqUMKqJUtggAgejWpIXGoxEr3ghfFJZCAgBISAEhIAQEAJCQAikP4GPe36KSzfv4pMf5kV3dvbwThxdNxdv1n8VXbp0ic6P7+Lq1avo06cP3N3dtS8HETjER8k881q1aoWIiAi88cYbqFy5MpYtW4bQ0FC88847WLt2Lezt7TF69GjznLzMyigIiKaDUSyDDEIICAEhIASSS4A2qPS8TUdYkoSAEBAClkCAzgCPn72InqMXxZque+4CCL53DysWzISV8tRWqVKlWM8NN7du3ULfvn2RL18+bdcvAgcDGct4b926NWxtbcFQmn/99ZcOo0khxPXr17WPpAsXLqBAgQLImzev0QChKcjNmzeNakxGA8cEB2JtgmOWIQsBISAEhIAFE6Bq6KuvvmrBBF7e1COf/qM7s7HKlOad3g5+hD8P+aF59dxwd7JN8/alQdMicPdhBFbuvwnfgHCERTxFFuvMyJMjK1wcbOF//zGqFHJCUMhjtKhhPJuil0WYzv927tmPdt/8Gm+XrzXvAI98Xvhtyjfw8/PD119/Havc+fPn8dlnn+loBTzNFoFDLDwWc9OiRQvwRT8Oc+fOBU1tfHx8tNlFeHi4zqtRo4bR8KCzUxcXFxjTmIwGjgkORIQOJrhoMmQhIASEgCUTiIyMxKNHj4wOQXBoBH7ddBWta+VFQfdsRje+lAzo0MV7GLbQGwFBUZxzudlhdKdyKJ7HISXNJFrW9044Zvx1GVUKu4jQIVFS5v0wPOIZRiw7j8M+gfB0c4CHazY8s7bFvceRuH31EcIePUBoWAR2nbqLgLuh+OvQbbxZxRNvVvCAo535f42dPHkyVqxchQ+/HA3XnHkS/DCUqPwqXmvZA+tXz9An159++qkuy/DCjFLBiD8jR45ElixZEmxDHlgGgerVq4MvagxOnDhRaz88e/YMNL+hrwc6nTSGFBYWJp9XY1iINBqD+f+1TiNQ0owQEAJCQAgYBwGqD8cX3m3ONl/8uuZS9CBLFsqO5q/kRlO1QUlOOnElGBNU/VHtSyOXc8rDx+05F4g/tl+HrTqh/axJkeR0aZRlzt8MQa/JR9XmLyu++bAUqOwwadUFnL52P02FDkY5eRnUSyWw7VQAJq65DBdnB/zvrfJwdbZ7rv8HDx/jzOUAXLwWpH7vbXDVPwyHzgfCxd4aDcp5PFfenDL69euHA4cO4/3PR8KjaPxmEzHnW7Px+3B2z4mV88bozdpHH30Eqs3XrFkTXbt2lQ1cTFhyrU0Ux44dC2rC8PMREBCAu3fvGo3QgcKP+P7Xy9KZJgEROpjmusmohYAQEAIWS4DqofGlx5FP8fTZP+j3vxJ4pNSz/zzghxHzvVHRyxl5XZ/fzMRtI+DBY3hfDkboo6dxHyXr/o0KuWCjjKprlnBLVnljLTRxzQU9tDl9qyKHQ9Sp6NuVOLdMOKr4jF8d9bx/i2KYvdUX/oGP0L5+frxZMReo7dF54hFYKw5llNCndc08KJHXMXqqK5T6/OIdN5A5cya8Xi42p3+UcGPW1qvYcPg2Ip48RT11kv3xm4V1v9ENyIXZENh4/DbGrbyAimXyoXrZhE/wnRxt8Ur5fPoVdD8ch8/44XpQCA5euGfWQof+/fvjtPd5tP50OHKXrJbsdS9VrS6sbWyxYPpQ7SiQfhwYoUCSEEiIQPHixbFgwQKj85PUsWPHhIYs+SZIQBxJmuCiyZCFgBAQAkLgeQJHLt3DyUv3MbZreZQrmB2N1EZ53hZf2GTJjOrFXJXjrKhN7Q9KlXvBjmsIUCeolZRqv5XaAHdTJ/ubjt1WqtxPcVCZFqxRvgZW7r+FWiVzwOdWCPrPOaXvi+Z2wI8rffC7atfezgpFPB1w7W4Yek0/jtUHbuHk1fvIrezQC8Qwrwh9/BSj/jiP8UpbYM1BP9iq8RTL7agFJOz3XlgkKhRy1hMKULbrPaYeU21bo0guByRU9/nZp10ON4Ku2W3RuUHB6EbJiCkk/Al874bjoHcgTvg+wOPIZ0pI8wTr9vuh0xuFtC3+dSWEsLO1wn5VZumO63i3Vh5ks7XGZf9Q9Jl2XNmTZ0KJfE74WwmFnig1iibKp4OnS1b8edgPE5b7gKYcLo5ZsPGgPzwUy5IxhBbRA5ILkybAz8J3C8+iSrn8qFomd7LnYqc0HQrnc8E/yIQjZwOg3lAmv1Oy65tKwc6dO+PKzdtoP2QWXPMUTvGwc3jmg42dI9Yu+x3W6ne3QoUKKW5DKlgWAZrfSBIC6UlA+bmVJASEgBAQAkLAdAhcuXIFfCWVHoRH6iIGu29uaml+kVVtiHMpzYcFm3yxVuUxVSicHQVz2evr4mqTW6Gws35lsckMx6zWKJjTHj5qkz188TncCX6MsMdPMGyeN5RihTanKKOEHEWUvwOW8bsX29/EiKXnsG7fLXjltlcn+M8wXGlf+CsnioaN/O8brup++eNvJfhgG8XzRGkHJFQ3ukI6XISERqJSMRfd8l9H/DB82Tn92n7mLih0+eQtL/2MgpF5fariq9YltaCAc6JmxLetSygTlTJYNqgGbJSpydI9N3X5nWfv6veVg2tiVLvSeO/1fPre8GPjUX/QJGbu51Uw7eOKqFzCFRuO3jY8lnczIvCjEmx55c+BSqWSZ/oUd+pli3oogUU+/L7ZF8eU9o05pc8//xzUuuo0dA6y2v+nJZTSOVap3xxNu3+LX6b9ijlz5iSr+sGDB+Hv76/LUuX+3Llz8dZjmfbt2+tXzDK9e/fWeatXr463XnIy6VugQ4cOOHv2bHKKP1eG5iTx+fwZM2YM/vjjj+fKpzYjODgYa9as0ZEg6DdDkhAQAokTEKFD4nzkqRAQAkJACBgZgXnz5uHy5cvxjormFd1/OYqOEw+j9Yh9emNft2yU3Xdim9qebxVG8xpRJ6484f+iWVH9crHPkuRGO6fy/zDg3WLo3fj5E0lqV+w6EYDObxfC+M7lsWxA1EZ8w3F1SqvSe0oLIExpCpzyjfrSuuGwv97A0xFlUnXjBZAGmQ72NggOidAtXQ0IwzGlxr5WCQ4OXgiK1XpFJahhqlbUBSu+egW5lbbCCaXpQfavfbkNb329C5FKyHLuxkNdbv+5ILi52OqoBMygA8mY6dj5e1rgUn/wTvB13OceTl8yrw1lzPla6vWSvX64FfQY9Wo8//uSEialC3ugVBEPTF0f/9+ClLT1ImXpd+HPP/98kSai6w4aNAjevurvxdCZsM7y4hFdSlWrg0btv8DG7Xvw008/RfeT0MX8+fN1NAM+3759u37FV9bNzQ0//vgjuPF++DDq95vlBg8ejKJFi+owjPHVS05e5syZ0bBhQ7i7uyen+HNlunfvHm//NDEpVqzYc+VTk/HkyRO8++672LVrF9atWwd+BiSlPYGpU6di8eLFad+wtJghBMSnQ4Zgl06FgBAQAkIgtQT4RZcx5xNKDLUXEvZEmy8MUSfqhkgS3NQycUPLFK5MBehbICUp7kY7qbo0zeDGe95GX2VqcEMX5/0hnyB0qJMf9ZUjvOGZvbH2iD/yKbOCS2qD/lHTqM1YUnWT6ju1z/N5ZMMh7yCEK78YFMbw1Xjo7ueayx/DhMTwsP+sk3DMZoPJvSopR3826DrpiOERCuTMlqgQwU5pTnA5BipNCUOyy2JluJR3MyGw9pDSaFECg7RItSsXxKzlh5SvkXuo5BVbiJUW7SfVBsNTZsuWDdOnT9evUqVKoVevXihQoEBSVZ97Tg2Hq7cC0GPELGS2Sruv55XrvQNHF3esnPI18uXLh/fff/+5vhPLePr0KRo3bqyjX7z33nto1aoVrK2t4enpCVvb2IKRnDlzwskptrnLnj17QA2KPn36YNmyZaAmw//+9z+cOXMGv//+u35/++230bNnTxw6dAjff/+9Hk7VqlVB4caxY8ewcuVK7RDTwcEBQ4cORe7cuTFw4EB4e3ujXr16oMNMjoljo0CakTvogPCbb74B/RV8+OGHuk1qUJQtW1Zfc0zc1Lq6uuKTTz5B4cKFQT8aFHZQ4NKmTRtdjwIGRhA5fPgwfH199fpy/KtWrdJMGHqSL3NOXAN+Dii4oQaMlZUyLSxSJN2nfOLECdjb26f4M5vuA5MOUkUg7f6qpap7qSQEhIAQEAJCIGUE+GWHXzrjSzRZmNy9AiKVr4AGX+3A4l038HblXLpoUptae+V3gCnwYQQK/2tqoTNi/Ihvox3j8XOXzsrDPlMZZa7RSmk1GBJ9GDBRQFK7vDs2H7mNoso/BFOzalEq50nV1YXT4UcPZT7x2S/H8MGYg/i4sZeO5PFIhTVkuhYYjrPXH+jrS8ou/7iK+GHwR8HMZ0rTxEEJD7IrwcM+JVihqYa/Crt5VglT6pZxx6qdN7RPjDpl3PDrhtgmMg0q59TPd58NRB1VNpsygzE4stQdyg+TJ3DBLxR37z/Cm69G/U6+6IQyZcoET4/s2HzyToYIHbjx5iaYadGiReAG+4MPPtAb127duoGb6eQkhrQ8e/4CPh6/OjnFU1ymWMWaaNrpC8yb/xtKlCiRqI8HbsSpuUHzhqZNmyrBbGZQu4wCls8++0yHWsyfP3+yx0DTA4M5HKMjcBPPxLYYMYECg+XLl+Px48eoWLGiNgUhQ4ZLZGJ9PqdpBIUUFEBQQEGhA8M7MvrC33//DToY5vMmTZpg1KhR8PLy0htWCiNoXjJp0iTcuXNHt8kfNAXh2l28eFG38csvv2hzEgo2Ro8erdeuZcuWOm/8+PHYuHGjFrSwPSb6QKDg5IcffkhTsw3duJH94Gd78+bNWnhEgQwFOi9D6FC+fHm4uLx8YaKR4Teb4YjQwWyWUiYiBISAELAMAjyVSiox0sKHDQpgxl+Xtc03I1gktamtUiTKqeQEFZ2ha6OCyGpjhWLKT0OY2nAnttHmRvyucgD5QDmEZLp8O1RHeSioNAZoeuHpboeTykShvFd2VFansfTrkFuFozSk92rmxY5jAZj+1yUUVU7x3JS3fqbk1DW0kZbvNYq5ghoiPymHm1/PPh3dtIdyLvmbEhRsVM4wmZZuvaZfBybUjy7T652iGLf8PNr+sF+FP7TFayoCxU5lSjJkoTcWfFEN5ZWviBXKuSRfVUvFjgXfp0lRhIY/1f4v6AODqVntPBj83n+aD9EdyUWaEAgPD8emTZv05pIbeKrKc1PBzWZ6pG3KL4hDNlvYK6FUWqVcHk44d/2/zWRy2qXd/4wZM/TJNTdQPO3mZpK+AHhSb9hYZsmSRV8zjGDevHn1RpibIJ70BgUF6egQJUuW1Pf/KHsoOmysU6eOtvP/9ttvtUlDoUKFMHPmTJBvfGn//v3YsmUL+o5diCjRXnylXjyvZK3GOLF1hT6hT8yxZK1atfTmn8IGJgobaJpx6tQp3L59G8ePH091lANqORja5Bq0bt1aCzUGDBig8/nD2dlZ84zOUBcMk0zOfD958qQWXFD7YN++fVrwwDWh0IGCAK4dBQcxHSOyTTu7/yIY3bx5Ew8ePECjRo1QunRpTJkyJbo7anUwj+EaKfAoU6aM1vSgOUXt2rXx1VdfoWDBgro8TSsovODYzD3xd4Gf05iJmis0f6hRowZo1hKTecxyqb3++OOPU1tV6hkhARE6GOGiyJCEgBAQAkLgxQl88Fp+zP77Csb/eVE7J0xqU0utA/pemLXuCgbNPKUHMKhtSRy5GJzoRjvmRpyVeJrPV28VUvKD1/JhinKKOPD305ijxjIHUaf7o7qURb1/fU1UVcKObMpZ5YOQSHR/O7ade1J1X5xS/C1QO4QvRs94pEKRuirfFoY90/C2peKvpHLfqeqpXwyd6azqUMDyrG1p0CEnTSd+/aSSbtNKNWar8hj9IquK5sHE9xEflALbv6Mc6VkpwZFoOmg06faDGzGeYnOTRjV1qolzM51eQoczyklqTrcojZ60mpSbsz1Onr2Zoua4oaYpAIUMVN+m0IEn8NxkUqBAAUJkZCQiIiK0WjlPyHlNoQw3X9w8h4SE6I3rpUuXdBk+pxCHz5k8PDy0Gdi1a9e0dgDV+eNLPK1nXQ+PnPAPVU5g0jHZKlMQqsknlsqVKwcKHk6fPq05UHuAAhdqFHTq1EnnGepzY87PCxOZUbBCAQDNEAx5/GxRcMF+KTDgJp7sqSWye/duvPbaa7psYj9sbGILqXbu3KnNLuiwkgKdq1evRlfPkyePNrGgb4mEUq5cUZo2NMWguQCFGYbE9Y+Z2De1ILi2Q4YMwezZs6O1W2jOwflZQuLaM6wmPxtMNHGkRgm1SCgA4lrQuagkIZAQgUzqj0T6/oVLqGfJFwJCQAgIASGQCgI8oWKKazuc3Kb4Xy+xTS1NM+6qTS83xa4qGkNaJbYbqMJ00ozDEFEjuW2/SN3k9iHlLJMAN8MjRozQ5gGvvvoq1q9frzfMK1as0JsM2sDTT4HBvp6CCZ5680R57dq10VoByaHXSfn4KJA/JyqUSBvzCvbpfzcUf23zxppvXknOENK1DLksXbpUb3opjODGmqZg5JqQLwUKMmhece7CJXQetSTdxrd/w3Ic3zAXX/btiwYNGsTbD0/t6beBgoBff/1VCxJo8vDFF1/ov7cUMtC3AU/4C6rTfpo18Bm1Rbgh5Yk/TSnoD4GCFPpxYHt8MZ9+GrYrTZodO3Zos4UJEyaAJhcUatCkg5vXJUuWgIIa9kXtArKj4IObfbIlY/bJZ/QpQVOPDRs24Oeff0azZs30Nfvl/4e+aq7kTwedFO4w0SyAfbGtcePGaaHBd999p7UYWJ9rQbV+CiI4P5qZsB0Kkyiko+CB2ixMNPOgzwhyM+dEXxc0W6FGCP2C8G8CtRr4t2LatGna5IUaUxTOSBICCREQoUNCZCRfCAgBISAEjJIAvxS+8sorCX5xNspBy6CEgBET4AklT6mpVs4NBk8x6Sxv1qxZ2l6d6uk9evTQoQx5gky1Z2oLpNTeus2YQyhfOh9KFHJLMxpXbt7DzoOXsFKFaM2IRG2JkSNHaqeG3EDzFP2dd97R2g3JtXsPDQ3Vm9njp7zRfshMOLmmLnJDQvPfuWoujm5ciF6f9NACgITKJZRPLYWYZ5QG8xOWp0YIn1PwYEi8N2iRGPJYzmBiErM+hS7czMfMM9RJ6p2CDdZjfzG1IdgXNROohWAwE4mvLWq3ULPBMK74yjCP68N2YppoMD+59VnWlJNB6EBNIAoghw0bpj/f9FtCgQ99YNABJ4UwaZko0KBwg0IsSaZPIEqn0fTnITMQAkJACAgBCyFAe9zUxnC3EEQyTSGQIgI8GeYJNDcRTIwiwI0WBXwMC0jbeSaqWFO7gSfXVKdOabLNYoWw8CjfJymtm1D5gMBQuDr9t+FNqFx65P/22286vKOPjw/odHDbtm36BJ2OEJMrcOC4aOIxZswYvFqzOpaP64Oty2ak2XBX/zoKO5ZNURoOn6dK4MCBcGPOzb3hFXNw3OzHFDgYynNOMRPLxVff0dFR58csm9xrah9QGBBT4MC6vKemQ2ICB5bjeJISOLAc5xJX4JCS+ixrDok+MMiCzOiQtG7dujqiBZ1xUtCW1onmL/zdkmQeBETTwTzWUWYhBISAELAYAqLpkL5L/VRFoLgaEJZgBI/07f2/1umgM5dyXklfG5LSl8C9e/dQv359HD16VHd048YNvUFlxABu3rjpZGhIQzp37px2wkfhBH0XJDcNXngWd0KAt19L2N4+uW0Zyi3b6K0iqDigX1MvQ9ZLe+dJO/0VpCZEZkKDpMr//EVLUbdVD7zaLCrUY0Jlk8r/a/ZYXDqyGdOn/oLEfBwk1Y48FwIJEaC2B4U36ZFE0yE9qGZcm+nzKcm4+UjPQkAICAEhYOYEGHs9LdI95ezwifKz4O4UFS0iLdpMaRthylHjA3Xym0tFucjoRL8RQxefxaZDfmDo0b3j6qX5kGZsVnbdRVxRvmD2JNv+csZJ+PqFoJSKPDJeOd6kY0pJ6UMg7mkvbezbtWsX7YeAGhA0waDjPH9/f60FQRtvOmBMSapTOgcmrolyppqSegmVDVG/w7cD7qPNh2knxEior/jyedKelgIH9kH/ATRnmTztV9g7OqNS3SbxdZ1k3t9zJ+Lqqb0Y9f0IETgkSUsKpJZAegkcOJ6OHTumdlhSzwgJiKaDES6KDEkICAEhIATSj8DxK8HoN/OkjhbRpbEXujcslKadHbscjKOqjy71CybZ7t5zgegz7Ths1Gl+v/+V0JEfkqyUTgV6zziBA6fvol2jgmhc2ROFVMhPpvqDd+KxEo5s/P41ZLO1wrQNl/HXAT+s+TbKi3lKhlP98y1or9rv+VbsKB3xtUFnnztUiMVxKnRndqcs+GtIbR0BI76ykpc+BGjHzygNtI2nYIKnmjS7oEp8UqrrCY2o5Q8HUK5kHpQv/uLOJP/cfh45VCTEnzqUTqg7k81fuGQZxk74GfVadkWtJm1TNI8NCybj2pn96P1RJ62RkqLKUlgICAEhkA4ERGcxHaBKk0JACAgBIWCcBHxuheCjiUdUeEYrjO9RAe1ez68HumTPDdTsuxWT/74UPXDerzviH32f3Iud3nfx65r/2kmsXrVirpjWuzJKFcqOkQu88dcRv8SKp9uzK7dDtcDhg4YF0EsJBAwCB3ZIgUOkCn256uAt3T/DXD6KSDzsXloMlBoo772SB8M7lkFQ8GOsP5rytUiLcVhyGxQ0UMBg0ITgqSYdu6VW4ECWrWrnwZHTN2M5JkwN4zMXA7SWw5fNi6SmutHXafu/Vqhf93Vs/eM37F2X/KgWGxdOgf+5g+jeoY0IHIx+lWWAQsByCIjQwXLWWmYqBISAEDALAgzvx/BcqUmT113SpgOL+tdAzRI5YKeED0zcVNOXwR87bkC96cT7SMNNVFaa/7RWZgwVlfnAtE8qIZebHSYrYUVGBLLedDJAz61dnSghTNyJUhNj0bZrcbP1/UW/UPSYegyNh+5Gr1+Pg74YDOnE1ftoP+Ew3hmxF8v23jRkR79vV5oMfM66/X8/hcCQiOhnhot6ZT2QLas11h+7bciSdxMm8H7tvCiS2w5rt59L9SxuBYRg9+Gr6NigAHIqvx/mmkaPHK6d9W3741dcPHEgyWmun/8zbp7ahc7t22jHfgwTefr06STrSQEhYIwEGLZ14cKFxjg0GVMqCIjQIRXQpIoQEAJCQAhkHAE6uTt+/HiqBnDYOxCNqnvCIWuUsCFuI2GPnmDLyec3tw/Dn+BrpYnQdNgevUnefTYq5jvrP1GCidGrfPQzbr7v3I+9cQ5VmgJDl5zVG++2Yw5i7eHntRmU7AHt6hfQJ/rXA8PiDivd72/cDUdWZTrhkoDfBDILCHoEChGexZGKfDT5CE5euIeCuexxSPHtOSXKGSEH3XPyUVy+8RDlCztjxvrLseZB84kBv51A4P1HKK18POw+cQc/LD8fqwxv1GE7CuZ2gH/go+eeSYZpEhj1YRn1ixOBNdvOqpCLz1I0iQu+Qbpe42o50apmnhTVNcXCY0YOw/9a/w+rpw9VGg+LE5zC37NHw2f/OvT5rBeaNm2qy925c0eHM1yxYkWC9eSBEEgNAYbVZdSK9EzBwcGg4EySeRAQoYN5rKPMQggIASFgMQTo5K5ChQopnu/9MBVPXgkISuR1jLcunSe+XtED87Zdf+75MCU0oINFT2VATueGX0w/DrbHtGT3Dfyx/To8XKKcQW6NY5IxYuk5rNt3C1657RGhNCqGz/eGf/DzG+gSeaLGdTMDNteBSgDgkC1h39J5XLNqh47zt8fWdqCWQ0hoJL5tVwpTPqqIvq2Ka+FEwP3H8LkZojVIRncth2FtSmFkB7XRjJE2Hg/QWicrB9fE6A5l8fE7RbBP+ZSIT7nE1ckG9x4+jlFbLlNCYPLkyXoj+uxZyjb4KekjJWVtbTIr7Z6K8HDIjLmrjmDnEV88U45ME0v+d0Oweus5bN57Af97LTd6vZ20X5DE2jOlZ19+9gm6dvgAJ7YsA7UZ4qYtS6bDe/9GTJ40EXXq1Il+zLCGzZs3B9d/9OjR0flyIQRelMDs2bMxceLEF20m0fp0qJpSZ7WJNigPM5RAwt8wMnRY0rkQEAJCQAgIgfgJfP311/E/SCI38EGUBoKLvU2CJdvXLYAu4w7hRtB/JgI82OdmmKf93DzTfKDV8L3YdvoOmlfLjR3qvYCnA2Z+Wlm3+6Gqf+HaA33NurtOBKDz24Xw0Rte2nTi1X7bsEFtuDvEMWVwdcyi69zNgM21m1NWnL58P0EufNCubj4MmnkKb9fMHV1uz/moU6jyBZ11XsV/36kJ8lBpjTCVL/TvMy8XfW/4sePUHS0EemvIbp31RAlkaObiGxCKQjntDcX0e9CDSLg4mq8afazJpuENNYKoorxjxw6Eh4fjwoULKF68eBr2kPqmbKwyY1S70vr3aOHOm5i25CByuNjD3dUerk52sFImPeGPIhEYHI6AwIcIDYtAmcKuGP1lVbj/+7uS+t5Nr2aHDh10+NLvR/2IiPBQNOs2UE/C++A2nNz5Jz7t+QlKlSr13MRatGiBsLAwMPygnZ0dPv300+fKSIYQSCkBOpnlKz1TtWrV0rN5afslExChw0sGLt0JASEgBIRAxhBw+9f2OzAkSkMhvlGUye+kfSvEPNG/oDQbuBmuoJw9MuVX2g7UitijTAkodPBWm/WGVXNFN1exiHO00IGOK1l33kZfLFX+Iph4f8gn6DmhQ9DDKKGIWwZsrvO4ZcUjZQYSrEIQJhSask6ZKN8Kmw76w+5f85QSeRz0nHzvhMFTaXpcuROq70sqbRJqOzDdVEKaYso8Iu4X1Oz/Cn/6vFs02rcGy+f6V2OE10z8XntNObos/S//qFz5mRiBoKAgjB8/Xvs+sbKyQtasWUENIWMROMQce90y7uDrphL0bT11F+dvPsQt9TsXFvEPstpYwd3ZFm+Wz4fm6neMpjaWnN59910UKlQII38ah7G9m8MtVwHcvXUVXTt1ROvWrRNE8+GHH+q1HzRoEHx8fPDzz89rSyRYWR4IgXgIUGsqvTWnUqPRGM9QJctICIjQwUgWQoYhBISAEBAC6UvAyc5aCwt8bj1MtKO2dfPj55UXossU/Dd05NWAKF8L3JhrM418UeYQbi628FP+DuJLzvZR/2bLKJ8GrWr9Z3/ODXrcxM0WU54czz+LWzat7xsoZ40z/7qMBTuvJxjOkn4nWr6eF/M2XIUSu+ghVCnsopn+sOwcyO33TVe1bwgKHQp5RGkr/PjHeXRrVEifaMccd+OqntipND6W77mJ9xSbvEqYQ9MKg3NPQ9kdKhoITTjeqOBhyJL3BAicO3cOs2bNwr59+7QJUoMGDeDt7Q0/Pz907949gVrGkZ3H1U5Fk8lnHIMx4lFwI/bjiKGYv2Q5zl2+gfadOqHDB+8lOeKqVaviq6++wtixY0FtMTrklSQEUkuAEWwMUW1S24bUsywC4tPBstZbZisEhIAQMHkCo0aNwsaNG1M1j8olXbFhvx/C1Kl+QqmZ2gw/i+FYIItS86bQYLXaHHNT3n9OlDf4hhVy6iZeK+uuHSky7OZ2ZWqx7sB/jiJzOmeFp7udfu6jTm8d7Wxgq05vcysfCTETT/MXqOgQzk5ZkC9HtpiPXsq1l3ICWUWxmasECrO2XAWdPMaXGHkgZqLGxxf046D8UIxdeh73lBPNgf8roYtkzZIZHd8qhNOXgvHZL8fg7fsAjIJhSHVKu6Fn86K4eP2h9nPBUKZDF3obHuOBct7JkKVfzTypubxV6T9tkuhCcqEJzJ8/HzzN7tatmw5nSVV6ajWcOnVK2/g7OTmhZcuWQstMCFDb4ZuB/bDg14nolgyBg2Ha9erV0+Y2hw4dQp8+fdL9pNrQr7ybH4F79+7h4cPEBfgvOuuDBw/iwIGko7a8aD9S/+UQyKTUHdPXIOflzEN6EQJCQAgIAQsh0Lt3b2273KNHjxTP+JyKpNBBRZDIo7QXvm1TEqXzZ4eNVSYtTJiy6gL2jqun2/x2kTc2KOHBVx+UwjtKCHHK9z4+//WEPnFngeav5cWgFlG28bfuPUKXiYd15AluwquXyYG9J+/iwIT6ui2qjQ/8/TR81KbbkEZ1KQuGguR/4AvKBOPndRdx8EwgBrUtqU02DOVe5judXA6cdxp7VBQJzsPAIjlj4DwCQx4jh4PtcyrwkcpBYHjEU1DThO9UmY+rJh+kQmVSe8TVIYvum322/vGAdtpZSDnYnNyjPDLC7CQ5czeWMr/99hs++OADZMuWTTsOXLlyJb799lssWrRIO2MbOXKksQxVxpHBBO7evat9Ozg6OmozHHv72D5UMnh40r0JEBg+fDj8/f0xZcqUdBst/6bRNKxdu3bp1oc0/PIIiNDh5bGWnoSAEBACQiANCHz//fegqvAbb7yRqtYOXAhC/xkntQ+Dbk280LVBoWS3w80xtRUoqIib+Iz+ELh5ZopbhpvvQOUk0t7WWrURZXax73wQPldhNrnJ/6xlMfyvVmxNgrh9vIz7RxHPcMHvIcoWiPJh8TL6jK+P8yr6hafSCKGwQlLyCXAzsHfvXq1KnzNnTnTq1AkUQHh4iHlK8imaf8mQkBB8/vnnePz4MQYPHgxGupAkBJJLgH9naLb1yy+/JLdKisuxbVtbW3Tp0iXFdaWC8REQoYPxrYmMSAgIASEgBBIhwNMVply5XkzdnhoImdWRe3z+FRLpPk0f0T/EXRWZgeYNSu4gSQi8EAGeOq5ZswZt27ZF+/bt9aaSmg+i5fBCWM22cmRkpD5FfvLkifb1UKBAAbOdq0wsbQnQJ8jNmzcxderUtG04RmurV6/WfiOaNWsWI1cuTZWACB1MdeVk3EJACAgBISAEhIAQ+JfATz/9hL/++gvUBKpVqxaOHj2qVeiXL18OT09Pk+TEEJ9Pnz6VU/h0Xr2uXbtqVXl+hkqWLJnOvUnz5kCA2jH379/XplzmMB+ZQ/oT+M+jU/r3JT0IASEgBISAEBACQkAIpDEBqiH//fffGDJkiBY4sPkJEyaAJ4SmKnDgHN566y306tWLl5LSkcCMGTNQo0YNLaQ6cuRIOvYkTZsLATs7O1BDRpIQSC4BMZRMLikpJwSEgBAQAkZBYMmSJXB2dkajRo2MYjwyCCGQkQSGDRuGrVu3ahOKmjVrRg9l7ty50demeHH58mWtWn3nzh3lcPUffW2K8zCVMTOMJh1KDh06VJvlMNKFJCGQEIGXES7Tx8dHd1+sWLGEhiH5JkRANB1MaLFkqEJACAgBIQCcPn0aoaGhgkIIWDwBhsbcvHkzunfvjpgCB3MAQ1MROsK0srLC0qVLzWFKRj8HhtFs0qQJfvzxRyxYsOC58Z49e/a5PMmwXALpHQBxz5492imu5RI2r5mL0MG81lNmIwSEgBAwewIMoSUh3sx+mWWCSRCgwGHhwoUYNWqUdhyZRHGTe7x7924d/jNHjhxYsWIFwsLCTG4OpjhgCrBoajF//nzE1JahCQ8/a5KEAAncu3cPDx8+TFcYQUFBYHhXSeZBQIQO5rGOMgshIASEgMUQaNiwIVxcXCxmvjJRIRCXAAUOixYtwg8//BDtwyFuGVO+p0kFNxtly5bVr9u3b2vHmKY8J1MaO00s6Etj8eLF+OKLL/TQGRHlxo0bOjqKKc1Fxpo+BFxdXeHg4JA+jf/bKgWOfEkyDwIidDCPdZRZCAEhIAQshkC1atXAlyQhYIkE6DXeIHAwN5MKw3oy7Cc3Ne7u7siSJQuqV68uXvINcF7Se+PGjdG/f394e3tj9OjR2o8OwxRv2LDhJY1AujFmAo8fP4a1dfq6BuzYsSM6depkzBhkbCkgIEKHFMCSokJACAgBISAEhIAQyCgCAwYMAO2ce/fujVdeeSWjhpHu/TLcJ0M3Hjt2DMePH0fnzp1x/fp1HaIv3TuXDqIJ1KlTByNGjNCOSnv27ImBAwfi2rVr8fp7iK4kFxZBgGaOkZGRFjFXmWTaEBChQ9pwlFaEgBAQAkLgJRHYuXMn+JIkBCyJwPTp07F//34dWaBly5ZmO3WeoNJrfYsWLbRzzOLFi4Mvaj0wtKOkl0PgwIEDuqPKlSujR48e8PX1xdSpU7XGAz+HkoTAy4hgIZTNh4AIHcxnLWUmQkAICAGLILBr1y6Eh4dbxFxlkkKABL799ttop5HNmzc3ayiMxpEtWzZUqFABtra20aepNLGgc0lJ6U+A0YEGDRqk/TpQwMDP3Pjx40FfGxEREdq3g6xF+q+DMfdAgUN6R6+gI1MRNBrzpyBlYxOhQ8p4SWkhIASEgBDIYAJPnjyJ3ohk8FCkeyGQJIHg4GCMGzcO77//vjaL2LRpU5J1YhagwGHHjh348ssvzS4sZsx5xryuVauWvqVpBdX5mbgJJkNJ6U+A0YEYGSVz5sw6fGabNm2wb98+7eOBdvx+fn5gNAtJQiA9CZw8eRKnTp1Kzy6k7ZdIwOo7lV5if9KVEBACQkAICIEXIhASEgIbGxsUKVLkhdpJbeUuPx/BO9Vzp7a6Wdc75XsfOZ2z4uyNh3B3sjXruSZ3cv369dORF955vwMu+93D8sXz8ezpE1SqVCnJJv7P3nWAR1V07QPpvVcCJPTeeweRohRRsCGKXdHvt6B+otgRBEVUEP1ElCJYqQJSROm995JACKSQ3nvCP++Eu2xCkt1sdjd7N+c8LPfeuTNnzryzuXvnzCnIUrFu3TqaNGmSdDfQ2cAKKjRr1kyTkQMp85C5QrHuaNOmjRWMUB1DQGaC4cOHU8eOHaWFw9atW6Vbm7+/P0Hxe/r0aWmRgvtMtQ8BWLrg73PUqFEmGzz+9hFQFi4+TOpHwLRhR9WPD4+AEWAEGAFGwMIQGDFiRI1ItPtsEk375Sx1be5Dy3depauJObTrdCI19Helfm28aWSXIHKyt6kR2Wq607yCYvpAYHPwQjIF+zjT1fhMatbAk5oHO1Obhu7Ur5UfOdjVPuPK559/npLTs+ntb9dSfNYNurv9CGrV5wj9b9YrlJSURAgMWREhhsOyZcvkTrO1ZqmoaOxKOXbVscBlqjkEEE9j6tSpBIudjRs30r///ktQ/MIKYsaMGVJBhDpMtQsBuFaY2r1iwoQJtQtUKx9tHfGFuWHlY+ThMQKMACPACDAC1UJg+c5oWro1ktIy8qheoIdI42dL8GnNzy8gF2cHSs/ME59c6trCh94b16zWLLDDY7Noy/E42nEqmeKSsikk0I3qB7hT0Y26VFB8g3Jz8ig2IVO4wxRSj1be9NjAhlTfx6lac6GGxgiG+J+XXqawLkOoZZ+Rt4kcF3mR1nwzle6/Z0S5KeE+/fRTaeEwffp0za7/bUyqWPDPP/9Qeno6Ie0hdqcRL8HSCakaEVNg5cqVli5qrZIPO9CrV6+W/vYHDx6sVWPnwZYg8NFHH1FiYiJ9+eWXDAkjoBcCrHTQCyauxAgwAowAI2ApCFy6dEku+MPCwkwuEtwFFv5zjS4KdwEbm7o0sFuYcO2wpXp+blSnzMY9Fte7jkSJXcAsev6uMBrWMdDk8tVkBxuPxtGa/XGUX1SHQup5U8cWQRWKE5uQQQdPXKPktCy6v28IPSw+1kqwYHjppZcoz8aFxk/5usJhnt7/D637fgbdPfQOuZOsVMSuMjIHwKVizJgxSnG1j4MHD6YuXbrIlJtQOKxZs4bgu2/JBBkXLVpEq1atsmQxWTZGoNYhMHPmTJllZuHChbVu7DxgwxAo88pkGBNuxQgwAoxAbkw0lf0Ui90+S6Fike0gZfcuKszIsBSRWA4DEfj555/ly46BzfVutk+4Ckz/I5xikvOpT+dQeuLeThQW4kUhAbcrHMA0yM+Vxg1tRW1b1KPZK8Ppp+1X9e5LbRX/OhJH32+OoqK69tS1fcNKFQ4l2LjRqDtaUsfWIfTHnlia/ecltQ1ZL3ljYmLomWeeIVt3/0oVDmDWuvsgGvHkW7Txn5303XffSf5YXJ87d04GjTSmwkER/oUXXpBBKb28vGjFihWELAXYqUTMBESKV4xfN23aJC0wkJoT2SRAkO3pp5+mgQMHyqCOCk9THhX3CkUuU/bFvBkBRkB/BMyRvQLBTOFmxmQdCHBMB+uYR6sfRX5iAmWdP0/ZEeFk5+NDzk2akSuCyNnUTv/pshMuF9QHSnJq45537z5UR/jCmpPOTrj/tu6CXniVAu+1jHzy5196gXIjzpOdXyC1Wf47CYfU2+TlAnUgAJNO5Iw3JZ0Tlg1f/3WFCm/UoU6tgqhlI1+9u+vSOpg83Rxp0d8XyMfNjoZ3si6LhwPhKbRg8xVq1MCPerYPkRYg+oLTvnkgOTva0Y7DVyjAy5Ee6WM9ATmhcHjyyScptFUnGvr0B3pB0rrHQIo4tZ8WiWcSFAHjxo2jIUOGmNQCAb74d9xxB128eFEqFGAeP23aNEL8ia5duxICBb7yyitSCVFQUEDnxW9vcXExvfzyyzRv3jzq16+ftDTSa4DVrITI9bm5uTKeAPBhUg8CSGuMTC0jR46U35cMofDftm2bvFbPKFjSyhAwtTIQzyZOj13ZDKjrnnlXJerChqW1EAQSNm6ga59+fJs0Lh27UaP3PyJbEWG5tiQ9cc8AAEAASURBVFPqwQMU9eHbGhhsZ88jjw7mjSjt1Lw1KZYNeZHhJbJYSsiYoiLKjy7ZdS5IiKNC8TJka+FmxZrJ5JPbEPDz86OGDRveVm7Mgnl/RVJ2XhHd2aspBfpW3QS9SQNv6tWpIX3712XqI4Ioujlaj4L0s1URVD/Yh/p0amAQ5E0b+lBaZj79uj2K7mjtTUFC+aB2SktLkwv1hs3b6a1wUMY86qk3qV6jlvTZV1/L4HywLjA1Qd769evLNIhQciArRM+ePQkv+Z6envIcrhgglIPeeustGfiyZcuWcqx9+vSR5ab8r23btoS0mUzqQ8DJyYmWLl1KyIIxaNAgqcRCtgMoIZjUjwAsHUxN3bt3p5SUFFN3w/zNhABv9ZkJaO7GMASStv6tUThgh9pr+GiCsgGUdfQAhb/xqmGMraxV+v59pUaUvm9vqWtzXLSY/x21WrhYfmy99d8VNodssIhp8NZ75NatN9V/8z1WOJgFdNN1gsXP0KFDTdbB6gMxdE1kpjBU4aAI1qllELm5udCctReVItUfpwu3EVgJDewWWq2xdGkdRO6uTvTNJtNarFRLyCo0njhxIuXcsKNhz92uINeHTedBo2nIQy/QJ7O/NGlgNlgsHD58WLpKIMZD69at6fjx43I3ESkQW7VqRe3bt5fKCAQL1KZnn32WYHkAa4x33nlH+5bJzpWd1KtXS5TGJuuIGZsEAaSL/eKLL6QbD3z/8R0CITAolGvvv/++DEaIsqNHj0plFr6Xw4YN46wlAMXCSfn7NJWYDz74oLTAMhV/5mteBNjSwbx4c29VROD68qWyhWPj5tR0zlzNYvH66pUUM3c25Zw/TRknT5Bb23ZV5GxF1YU1QcaeHXJAPvc9REkrfqb0PTuJnptkRYOs/lC8hMsJPkyMgC4E1orgiM3C/AyycCjLu1eH+rR+21nKyC1SvbUD0mLuOhlPw/u1KDtMg667tAmmv3eHk0hyQXVNv2lmkIz6NHriiScoPZ9o0kff61O9wjpQPCTFXaXlv62ggIAAwgu3sQmuEf3796c333yTGjVqJJRibtLkvXPnznT33XdThw4dCHEUsDgcPny4yNJiT0hRC9eKbt26kbu7uxTp0UcfNbZo5fJLSEiQcSY8PDzKvc+Flo0AvjOwnEFQVHyf4boDN6Q5c+bQDz/8IOOK/PHHH/Tcc89J5UTTpk3p119/lRY/+B4yMQKMgPUgwH/R1jOXVjeSTBFMSzHTD352kkbhgIEG3HMvJf7xK+XHXqPEP9dqlA6Rn86kvKtXyGvocPK/+5YJX5pwP4hbuohsXFypyYxZpbESi/b4taspdfs2ocQ4QzZu7uTYWJhUT5hIri1uvVwjCGH8r8s1bb3vGkEenbvQ9TWrKe3vTXSjqJDcevWlBs+9QHVFZPCLb75Oxbk55C3k8BPylKXLMz4W8keTW/eeFDze8FzEWeHhVJieKtkHPzJBKh3yo6Mo73ocOQTc8iXPj79Ol6cJP2MbWwp7532KWfIjZR06QMVZmeTUpj2FPP8COQbXKyVm/Po/KeHnn+TOZl1HJ3IMDSOX9h3Id8gwqmtnV6puVS5yr12lK7NmyCb1X5pMzo0bl2peKHKAR7z1hizzH/8oeXXvIc+L8/Mp6d+tlLb9X8oTPIoz0uXYYVlh4+5JjT78mBzrhWh4hU95g4rE+LTJPqgehU255Yqifa+q/LXb8rl1IBCTkkPXk3NoSL9WRhlQsL+bTKm55dh1urdHzcQvwEv+Tz/9RAgiWJ1sBct3RpGnhzPVDypZeFYXoLB6XiIDSB3aLwJ29mzuXV12Otv/+OOPFBsbSy+++KJm8ayzkY4KUDjEiUfMcx8vEY/W6r9SDXn4RfINDqUFPy4gZMHAnBmLlICQ2vzgqoQd6MLCQqlsUO4h+wZwQqBJKCZgSn3kyBGR+rRAXiv1TH0MDg6msWPHEisdTI206fgjPggClcLiAQQXHnyvkHIR33FfX1+pdHj88celMmzr1q302GOP0cSJE2V9/s8yESgSbquOjup3jbNMdK1Tqur/QlonLjwqC0Ag92qUlKKuo3O58Qk8Bw6m+OWLKDfyVhT07HNnpKLCtWPnUiPIEwvu7NPHCbxKkVA4hL/7trAU2K4pLs7NJvj9Z+zbSQ3fm07e/frLewXJSZKHUtG+Xn3KFGapqZvXKUWUsm6VVDg0mPQfshXa/dSt+6hILIzLKh0KhI9a6t8bZDufUdVLiZZ2oMS1wq17bxEx3YOcWrShnHOnKE0ElvQfOVojW2FGpkb+6AX/KyU3xn/x1HFqtex3snG+hVGuCNYHxY5CCMSYunWjVEQ0n79A9GfY4gPKDShcCpMTKWH9Wmr4f68oXchjxtEjGlldxG6cJGEWfOmj90vNldIIfPCxFUolbco6cVQofrK1i+R8lCpQLgzgrzTlo3kRmD59OmFn1hQuFvvOJ5OHm1CwORjv59HX242OXEqtMaVDuFBMRkREyACFSDOKXey+fftWedIOXUylJvWNG8zP29OFjkWmmUXpANeBAwcO0P333y93+eFG8NBDDxmsgMD3MPxqHL08ZyXVNYLCQZmQTgNGUMPm7emnj5+TL/UITmlqKm9X2cbGphQ2WGCYe5HRRASMRspMuHVA8YEgmDjiA3eQdu1uWTmiDFT2iDK0U0hpizgW2qS0Q1nZc+Ua/uV2QuEORYxiWo57ONfuoyxfpS6C4iHWAZRfQUFBspp2e9RT+lLO4RKjlKEBAoDCIgALdh8RWBuk9K/0Iwu1/lN4pKenyzlNTU0tpchRZNBqIk8VGXBU2kJhgDFot4EsqANrmV69epViExoaKuVUAoHi2Q0LmhkzZkhZ8D0DDRgwgPbt2yezt+DZftddd0nLiFLM+MJiEMAzA0FeTUm//fYbxcfHSwWoKfth3uZBwHhvVeaRl3upRQgUiAj1IDv/ALE7X/KjpD18u8CSXfzChNJ+p9p1dJ2n7NqpWcS69epPPsIioSAtVVpR5EVdpui5n5Nnj55UV/xAevcfQK6t2lDipg3CmuAXyrlwjorEQrfB1I/IsX4DujZ3DmWfOkZpWzYKO9v/kM/do+QCHXxgtaFtNZF26KBGNM+bu/iagiqepO/ZJVu4dSuxBsARSof0vbtLKR202UJR4j16LHn27E0pu3ZIZQmsJWBFoG0h4n/PGPLs3ZduFORTTlQUZR49LPGCIuLq13MrtBjQ7qvcc/EC6D1yDMUvXkCpAs/6sA4RGCuUdjMmBdxq7P38ZXG6eOlUlEPOrdsTLCAcfP1EOzsqzi+gwswMsi1jgtv0q2+pWOzggRKF1UbK+opzvRvCXzLm/8yOQJT4LuIF3xRKh8j4bLGb72TUMQX4OFNkVLxReVaFGUzq8cEibfHixTJTAfCDKT2CCGLx7e2t29IgLiWX2rfxrErXOuu6uzpQXHKeznrGqICgZPjs2rVLLm7WrFlDSMkWKH5LkM0BqS71JbT9Y9VaendpyfNX33b61vMJqk89Rj5OK9f+Tli0Qb7aSFgsY5GPBS3OYZGhnOeJlMzIiKAQyhWq7ByLZbSLi4tTqkueygXaVtQe7bBIxqJbuw7a4lp7IY5zpVyeiP8gM8aDQJ5YxGvXV+qU5VuWB2Jt5AurP/CqaAxleSn9oA0W/Fgs6lIglZUDCpOK2mQK60Q8U2C1UJYUHJTykJAQmjBhgsZ9CGlY4a6DWCEYD5QacOnR55mk8OSj+RFQvlOm7DkyMpIQfJTJOhBgpYN1zKNVjqIgJUmOy8az/JdhO8+SHTfpWiDMvMpTTOgCJmFNySLUqWlLavLRdE1119Zt6fyT4+XuebbYJXQVO2SwIsDHTpjyg+D6Ue//XiOfgYPktfewu6TSAfLcEC9G7mIHxj4opMQFZMO6UkqHjJvWCQiKaSt2TAylwvQ0qWBAe/eOnSQb985dKX7J95Sxf7dYjOeXWswr/UDhoFgXeHTqTBk7/pVuCrBs0CZYJCguFx5duhKJ9JdR8+dKpUv6rm2iavluCto8Kjr3G36XVDrAEgHKH59BN1+qxYubEqPCo38JtuABSxOFgp58htyFm4cu0nbbSNu3p9LqhvCvlCHfNBkCDRo0oI4dO5qEf3p2ITnYG/en0cXJnvLyi6ssLywUvvrqK7lAgXk5FjtYeMHEHdd4yY+OjpZ8YcGAnSdcYxENNwrcxwdmsNjZxM5qY+HKBJN1KG4uXLhAmzdvlnUQUBBKCPjuV0S5eYXk52lcc1pbMaYcMZ7KCFYaUA4opv7w80fMA+w6Y/dUWdQAL+yMg7BgQn3FnQQvrqiLRaOyS92pUyd5febMGZo/f74M4AjsEMhuypQplYlEm//dRa17Dqm0TnVvdh86ls4e+lf6vddWpQN29N99993qQsntaxABxHSAa442wW0IcR6grEB2C9DPP/+s+RvHc4vJshGAdYu20s8U0iLuDBR8TNaBgHHfrKwDEx6FxSBQsktAxUKhUA7dgKJBoZs7Csqlvse8KyWuGc7t2lO20KhqE1wxsCDOi42RSgfte8q5902FA669+g0gp0YlL7x18IMpZPK5ZyzFfvMFpW3dRMWTXhTuHeKFXcgtAz2KNp7CeqI6lHbokGyOmAZODUPluZtIZ6bInnHiOEllQZlO3EUcCQ1h16ZFa8o4IJQU4iW9LAGXrPNnKV+YuIHqOpQsOoBNoXiBN1RpAgsGuIRAOZIklDKK0iFLLDCUGBVeWinZPLp114gW8eoL5NqlJ7l26kJuIjWoq3gxFSsnzX1DTkzN3xCZuE35CCB7hakIMQaKqq4fqFScQsHQ0ECJeOnCziYCsEHhkJ2dLTMNKAtn7FpCCQHFAo64jwU2dlJRhg92h3ENk25ltxhHBxF7Bi90ePHfsWOH3HmtTOkg1vFkK/7MtJ68lY5bn5sFhUXk4lD53y5kxc4sPlAwQJmAXWIcFdNsKBkwdoxT2aHFNcYOQn0sZDBWYAI+yq4vlC2nTp2Su6zYWYNCRhcV24jd6kTTW68429WRO+O65NF1HzvIb7xREicHfz8ttOIV6WpryH3EEcH3SzH/Bw8ovnbu3CnnAek2OU6DIchaTxv8DeI5phCUpvydUNCw/CP+thWFkamkLeuqY6p+mK95EGClg3lw5l4MQMDOu8RXsTCpxM2iLItC4QYBknEaDFlwipdUxAEAwV0Cn/KoSJjtl0foF5YPCtmKnUXX5s2VS3n0FabLUDpggZ68cwf53jmEMoSrBa5Bnr16y6Oh/ympMeuI4JBXv/n6NjaI91Ce0sFJ5GfXSeJFP+Kj9yl9x9aKq4qX9+qQz8h7pNIB6U+VwJdpN9N/wkpEUaSgD+ALV5aY+V/Kecs8tJfwiRP3HBqEUdBzL2oCThoik6n5GyITtzE/At6udhSZaFw/1ZT0XPJwta/yYLBrX5kSoMoMbzbAwhtB3M6JZxEsAWDlgNgGAwYMqJSlq5MdJSRnkrfPredepQ30uJmZnU+N/SrfycJu98cff6wHt6pVQXyHf//9VypcoLAZM2YMIX5CvXr1dDK6b8RQeue9D+jc4Z3UonNfnfUNqbBt2WeUmRhDk54oURYYwkNpA7P3mTNnyngept6dRJ//+9//qLn4PXz44YelCFD+AN+uXbtKZRHcU7777jtFPD4yAowAI3AbArDKY7IeBFjpYD1zaXUjsfMpcatA/IBCsaOFRaE2ZYdflJd2gbofSkUiiOJtJLTssBCA4gELXMfmLW+rggJkbCiPkOVCF0Ep4T7gTkrftoWS1q2RSof0m64VTs1bk73P7f6Punhq7gulQPru7fISgS8T/1iuuaWcZCDeg4gvYQjFCdcTReGAMbi0bE11nRwpJyKcktf8YQjL29p4CeuFayLrBCwbEjf+RfUee1woIUrcIDxEoNCyBFcW7779KP3USco8IuJLiOwbSJuKuBmRb00mj3V/CxkrX8CU5al9bWr+2n3xuWUi0DzYlf45YVwf0rjETOrYsPTzy9yjx64+gnIhUNuxY8fkDvTEiRPp3nvv1VuURkEudOlaklGVDqlp2dS2vp/eMlS3IqxAENfixIkTdEW4k8HlBIvhp556qkqsBw/sT1cuT6D/zXuXHnztM2rUunOV2uuqvP23b+jcwW309ttvE6wCKiK4HiBWB9IRLl26VCoVEMxv5cqVtGzZMmrbtq0MwgalA8YK6wNtgjUIUhfCzWbYsGGyPfCBBQwUUXBt+eSTT2jBggUyswBcUQYNGiTroQ76vHbtGu3du1fGBnnvvfcIn1WrVskggfjO4TsGn/3Vq1fLnewlS5YQPkyMACOgXgRgqaJYlal3FCy5ORFgpYM50ea+qoSAm5bPPoIdameAQKwCpKkEuXW9ZXaPVJWgfGFGrE25l0vcKLTLcO4Y1oQyhdKhOC+Xwl5/s8T9oWylCq7r6Bmt3HfkaKl0QJDJ3JhoGeARLLXjFVTQRaXFmefPaywmfO57SKSeu2WijPEkr10h40nkRl8rlUayUqZaNxNvpgf1ue9BQjYOhVKEJYIupUNdp5IMGAUpOhZvwrXDe/R9FL90ISWvW00BY+7VZK1AAMvyCLh7CJcKfOiJpyhbvBSff+ZRWTVpx7ZS35Py2usqMzV/Xf3zfd0IzJo1Sy5wTBFIckiHAPps5UWKT8omfxEA0hiUkJRBfYaFGIOVQTw2bNggg0ciXgGsBrAYDQ0NrTKvQe38aMGmSOrc7laE/Soz0WqQmZVP2Tn51L91SbBYrVsmOcXCF3EfsAC/88476f3336eGDRsa3NeTTzxOqeIZt2XxTGrbbwz1GvGQwby0Gy7/9DXKiY8gpBrUlWXkzTfflEoJjOvVV1+luXPnyngnc+bMkcqEFStW0B9//CFTEmr3oZwjjebZs2dlPAu4XfTs2ZOQ1QHKDCgdNm3aRIihAkJf8OOePXs2/fXXX1KZAJedQ8LND/3BTx8KLfCB5UizZs1kcEC4AIFgOg8FCZQYkIuJEWAE1IsAlA6mJigqodiAYphJ/Qiw0kH9c2i1I3AQWSuc23Wm7BOHKearz8leXHsIRUSh8M2NnDlds+D2GngrqrddULDc+U7d+hel3z2SnEVwtcS/t2jSUwIsbasJb5GtIvPwPmntgHSMPqPuIfe27WTayGL4SQszZDuvkoCVhgINmZWAkrFLlxDSToK04xUYwltJlekQ2kQoBV68jUX6ru1yXKkidWbgmKoveOrcfFEsEsHahCO4jJmQL144Y7/7RtMXMn3IjBFlfnzshfVJfnSUyEyxnhAw0rGeCKgp2hYKn17t4I5g5HvX3VLpAIuTq9/Ml7xthfVDWVcVBM3E3ON7oGS6KBY+3im7d2rkQQBPQ8nU/A2Vi9vdjgD805X0a7ffrX5J28ZetOd4FN0zqEW1mZ28eJ1cnWypS2PjZn2oimCIWYCFZ3VNVaGQWb79Kh05fZU6tylZiFZFjrJ1D5yKoab13cne1vQvr+gbSgYEoFSCS5aVx5DryZMnSzeC6TM/o+ysdBr8wLOGsNG0+X3OG5QefU4qAeCeoIucb6Y4Hj58uAwEChcZuC4g3gVcaKAkgJLlueeeK5cVLBTOCwU2rBMuX75MBw8elFYfiL+A6/Xr10uFFdwj5s2bJy0awBN/f4qVDIJcIkBpSxFPCEqILl26SEsJyFbWRx8KsP/7v/+TdcsViAsZAUZAFQggNo7y/DGVwMiYgucQKx1MhbB5+d7aGjVvv9wbI8AIMAKMACPACDACjAAjwAgwAowAI8AIWDkCbOlg5ROs9uHV/79X6OKLz0irhkuv3TLxV8blM+b+Ujvi3oOHSFcG3I94dZJSjdx69RdpGLdLPidHDaEGb38osyX43DGYUv75mzL27dR80EjJ/uDcpgM1/7IkQOO5Sc9IKwqFKWJNHL2jJBAkUmf6j67A/EtYAXiPHENx382l1M3rZHMEPsTuf3Uo/eYOv1u3HuWycevWi1I2rqWMvXuEpcN95daprNCtRy8RXFNYK/yziY7v2Ul2wsIAsRPs/ALJvl4Daclw7onx0oqj9U+/lmLl0X+AxoLk7KMPaO45Nm5OLb/7QXONE1i0IBMFgkIq+HgMulNm/9CumLx9G0V/MUu7qNQ5rCM8e94KzBm9ZJFMyVmqkrjAGJR5w722azaRrUjZVVX+ZfnytfkQwM6tYvJtil6n3NeEJnx2mI6di6MOLQIN7gJmocfORNOILuZxH6hI0OpaOGjzfWxQQ/p8TTh5ebpSo5Dy0xlr16/oPL+giMIj42nKuGYVVTF6OTKBmIJGjBghrSemzZxN9iLAcL/RE6rcTUF+Hm3+YRqlXD0nrRL0sXLQ7gR/Ewp17tyZ4NIwY8YMGVdByfCB+4g4r+S9x/ezW7du0hoCLg92dnYy5gOyfDzwwAMyGCSyfsBSBgE3jx49Kq0oFi5cSJGRkUp35WbXQLYVxIMoS+PGjTN5xPuyffI1I8AIGB8BPFdgUWVKgiVF2Tg0puyPeZsWAbZ0MC2+zL2aCMA9ovHn88it263FJFhCKRA06ZVSsQZQ7tWzFwU+9QJONeR19xjyu6d0sDTtdJtNPppO9V59Uy6mlUZKdomC68iNUELVMd33HTJUYSOP3neNKnVd1Qu4GShuGu5dupbb3O1mOdxH9JZd/IgoFPLE0+T38ESZGQJ4YLGO4JcN336PbL1uLTbK4+07ZBgBd33JV7i1aJP3kOHal/K8QJj7VkRQKjX+7Cuy974ll3QJqaiBdvlN15Aq89fmwedmRWD8+PEmNc/2dXWgx4eG0d6jURQepSMuSSUj37AznDycbWmiWKhbCw1s60djegbR33vC6eKVJIOH9dfOC9SigTv1bXVrsWwwMwtoOHDgQJr8f5Po8OZfaMfqxVWW6Md3HqW4iFPSheGee0o/DytjhqCRcHcYO3YsHT58WFYNCQmhCRMmyOCSd911l3StUXiMHj1axoro1KmTzF4yRGRY8vT0pMGDB8s4FxcuXJBVR44cST///LPkC99t1IeyAgoW9LN8+XJau3atwlZzhMICBLngmoF2SqpOlH/66aeyLc6ZGAFGQN0ImDqQJDIJwWWMyToQqCO+MCLzNhMjYPkIxK//k6I//0QqHNqv3UgiQXvFQgtfs9yEeLIXi2MEl8TCuCg7i+rY2sl4ABUFgSwWEd7zRTsoJWzE7rc90naWiVdQcaeV30ncsomufvKhrNR29Uay1cpPXXnLmr+bL9KW1nVwlBYBkKZQxLoA1RU7Y/iIxOvyuux/wDMvLlbgbitiP3hq2peth+vLM2dISwenFm2oxdf/K6+KjMdRmJpCxfkFInBmHUIGEXsRnKzS70K5nMovRLwPU/Ivv1cutVQE5m26Qhv2x1LH1sHUVXyqQhvFojo5JZNmPtaawgJqNnNFVeTWt+5P26No0ZYr1LZFMPXr3EA8JvWPy/DP/st0LVZkkXipM7mJeBfWRLAY2Pj3Nuo56nHqPkQ/C7M1X71B2UnRNHXqVGlBgCwSsDqoLuH1DulRXcVvmfb8IMgj/LEdHR01XSBmAzKc6Ip3kS+CONuK5zna65KxWMQCQnpO8EQbEPrBDqm2PBoh+IQRYARUg8AXX3xBSDsMBSQTI6APAtb1a6/PiLmOahFwalCyW4hd9wyRX95N5JavkMRLjWNgkOa2XPSK9JW6CAoKx5D6uqpV+X7GyRMahYPPPeNUpXDAYMum9oQ7gj4EPJ0ahlZeVby8xq9bq3GtCHhwfIX1kTa1bOrUCisbcMPU/A0QiZvUIAIvDm1IAR4O9NM/kXT5agp1ahVEDYM9yM62YoVnVGwa7Tt6hZxFwP55z7Ynf9HeGumR/g2oeT03+mJtBC1Zk0StmwaSn5eLxKei8aak5dKuI5GUmpZF0ye0tjqFA8aNDA/I2vD5V18LXWxd6jq4couvDfOnUFpcpMw0sWPHDvr9999lMEdk2mjSpElFUOpVjoW9WznKbSgLyioMoBRQFAOVMVcyUSgWDZXVRZ2ygST16aMynnyPEWAELAMBVhxaxjyoSQpWOqhptmq5rC5Nm5Gtt6/MyBD+f8+Qe787yKVNO5kdweZmBG9Lgihh4wbKFfnL07dtlakrIRtiOQQ/9rgliVkjssA9JOmfvykn8jKlbdmoyUTieYdwy+jdp0Zk4k7VgwDMtrF4MUXKzLIojOsRSF0audO3wuph3/GrtPPwFQr0c6MAHxdycrAjG2FxU1BUTClpORR7PZWycvJoYHs/eunu6i0Yy8phidddm3jR4pc705JtV2ndgVjy8nChw6djyM3VnpqH+klsnJ3sKDYhQ1g2pIhjOjUJcqFPxncgP3frVMZgnpDVAVYGMz/9XE5bRYqH9f/7gJKuCaXNkiXSGmHUqFHS+gDpJ2EhALeEoKBbynNL/A6wTIwAI1A7EUhISJCWVKYc/bZt2+SzFO5rTOpHgN0r1D+HtWoEaUcOU+Q7b2oWqRh8+/VbhcvFLTNRSwHk+N13lpIT8RCafPIp2ephcWEpYzCVHNmXLtH5p0sHW/MaNopCJ79eoauGqWRhvupD4OOPP5a+4kgTaE46GZVOey6m0fGIVEpOzxMWD3VJ/JNkI45dmnrS8E6B1NDP2ZxiWURfmblFdCgihbafTqJz1zIpP7+QsnMLyN6uLrk721GjQFe6u0sgdWtavRTEFjFYPYVYunQprVy3iUb/5xPy9C0dkHT5rFcoK+EKLfphIfn5+ZXiuHPnTlq8eLF0Q5gyZQqFhoaWus8XjAAjwAjUNAJfffWVTKGL2C+mIqTYPXnyJP33v/81VRfM14wIsKWDGcHmrqqPgEenztT65xWUemA/5UVfE779+RapcMBIXXv0proihoRTk6Yiq0JPk7htVB/RmuFg6+EuM1bYBQSSo3Cb8ezenRzrN6gZYbhX1SEAs059zLuNPbC2IvAhPnRHfcrNL6bU7Hxh5XCDnOzrkq+b9e7c64Ojq6MNDWjtKz+on5MvfP6FJsbWRv9YD/r0o6Y6COYYHh5O3/53PD09bRH5BJW47m1aOoecKJdenvLmbQoHjK9v377k5OREs2bNovfee48efvhhs1j1qAlblpURYARqFgFzuFcgBgw+TNaBAFs6WMc88igYAUaAEag1CGzatEmO1RzuFbUGVB6oyRD4bPbntHr9RmrQvBPlpCdQnfwsev6ZJ2W2iMo6xQ7fO++8IxUQsHho165dZdX5HiPACDACZkPgyy+/pGPHjtGPP/5osj43btwoA9wi6w6T+hFgpYP655BHwAgwAowAI8AIMAIWjMB3331H/2zbTiHBQTRmzBjq3bt0GuiKRL948SJNnz5d+k4jxkPXruWnSK6oPZczAowAI2AKBObOnUu7d++mX375xRTsmacVIsBKByucVB4SI8AIMAKMACPACFgHAlevXqUZM2ZQYmIiPfPMMzR48GDrGBiPghFgBFSLAJQOu3btol9//VW1Y2DBzYvAzRBY5u2Ue2MEGAFGgBFgBAxFALu/+DAxArUBgfr169MHH3xA9erVo4ULF9K///5bG4bNY2QEGAELRgAxHZClx5QERSs+TNaBACsdrGMeeRSMACPACNQaBH7//Xe6JDKgMDECtQUBZLj48MMPZQpNuGoocU1qy/h5nIwAI2B5CJha8XD06FH69ttvLW/gLJFBCHD2CoNg40bVQaC4uFhqRxGRFhHo7ezsNOyys7MpKyuLnJ2dycXFRVOemppKKSkp5OHhQd7e3pry+Ph4Qq5gHx8fyVPJaR4dHa2pD01sgwYlmREiIyNlPfDy9PSkRo0aSV75IgvGlStXCPdRt3nz5po+zpw5Ixc4qNuqVStN+alTpwj9BAcHk62tLbVs2VLeO378uOwjLy+PHBwcqEOHDrI8JiaG4uLiZDTzxo0bU+fOnTW8Dh06ROfOnZP9avvsHjhwgM6ePSt5d+vWTVN///79mvJOnTppMET55cuXKSwsjEJFmrWAgADZBuXAyt/fX2LYokULTTlOcnNzZX747iKLBCg5OZkiIiI0fWjLBFnPnz9P4KE9hiNHjtCFCxfkGDp27Cj54D8EGsKudLNmzSS2Xl4lKfOAH3AHRqDWrVvLI+riuwGTYsyFIituIhJ8VFQUNWzYkIChQpg34IudQNxTCPODcQcGBpbKd4/vDMbo6+srq+L7A8L3DD+i+A66u4sMG66usjwnJ0eWQy7Iq/2dxffLHFGcpSD8n0QA84S/JSZGoDYh4ObmRjNnzqTPPvuMFi1aRAUFBTRixIjaBAGPlRFgBGoRAnjXxHsck3UgwEoH65hHzSi2bNlCWNQPGTJELoSgJVyyZIlcRPUUaRsff/xxWReLqldeeYWKiopkyq5PPvlEw+PRRx+Vi/6MjAxatmyZphzmnVjQpqen04IFC+RCHzeXL19OyNMLXu+++y716NFDtjl48CC9+eabUnnwwAMP0Pjx42U5Fvxjx46Vi0Ms5NFGoQcffFAuDrEY1Y6IizpYGGMBuXr1aqW6HBsW+ag/Z84cTTnynK9atUouDl966SWN0gELlW+++Ua+rCHyvaJ0wKIRL3MgKDyQf1ihefPmyUWwjY2NbKuUow8oBaAcmTRpklIsF64rVqyQ8zBo0CCN0gEVkLcd44fyQnvBDvyAN+ZLe4GPHV3gjQW6ttLhzz//lIsuKES0F/h79uyh06dPExb2r732mkYmlB0+fFiO7cknn9SUA9P169dL/Hr16kWK0gEL6d9++00qdDDn2jJBVnx/wE97DBgblEb79u0rJRP4Q1kAvLBTpxCULH///bfMRX/fffdplA7o+4svvpB9QB5tpQO+p1BUAXPt+UZaOaSYQ//4viuElHNQGABD7WBHK1eupM2bN0vlEKLDK0oHyISgbZAB30XlOwu+SFsH5QWUJ2+//bbSBQ0bNkx+l/G9gdwKjRs3TvKFggPfB4U+/fRTqYgBz9mzZ2u+g9i5xHce30X8DQ4fPlw2wY8uxoG5GjhwID311FOyHCaH+JtMS0sjKJ6aNGkigzrZ29vLeRk1apTSpdUdoVjCXDAxArUNASg8kckCkeN/+ukn+ZuBZxgTI8AIMALmRgDvK6bceMG7clJSkrmHxf2ZCAEOJGkiYGuC7ffff09bt26Vi7eHHnpI7gRjlxlKCPzRYpe7ffv2UjQsxLA7jF1jLLybNm2qERk7/licY9c4JCREU44FPxbM2B3Gg0axOMCCDgso7MKgHIsvEHZhcI3dYSyEsFhkYgSsEQH86OKj/R2HpQv+LmAZofxNYOxQEsDKApYUaKNYW8TGxsprlOFvFn7cILgRoOzatWtyoa1Y4eDvEX+nsAzB3zDqZGZmSgsRmGJD8QiCgkjbakgW8n+MACOgegRgdrxt2zZp7fDII4+ofjw8AEaAEVAPAtiQQyBJbDxqv/uoZwQsqbkRYKWDuRE3YX8wR1cWKibshlkzAoyAihCAVRIWJhMmTJBWGSoSnUVlBBgBHQjAcg8WY3fddRdpW7GhGSzxtC3kdLDi24wAI8AI6I0ALEzh/guLK2wsMjECuhDgrWddCFnwfZisa5uYs8LBgieLRWMEagiBp59+muBKhF2JkydP1pAUxu0W7lT4MDECtR2B559/Xrpjbdy4kZDCTpvghrFmzRrtIj5nBBgBRsAoCMB6Gm7H+DAxAvogwEoHfVCy0DrwB1dcHCxURBaLEWAELAABxIhAXIi2bdtagDTVFwGB9CwpbSAUwPgwMQI1gcDo0aOllQPi5yixiSBHmzZtaN26dTUhEvfJCDACtQQBuHaairZv305Tp041FXvma2YEWOlgZsCN1R2CJCIzwWOPPWYslsyHEWAErBgB7cwrah8mYtEguKqlEIKOYuHHxAjUFAIIaAvLBgTyRYBZxIVBIGdkFELQXyZGgBFgBIyJAGK2gZSjMXkrvBAEHcG4mawDAVY6qHQex4wZQ++//75KpWexGQFGoKYQwAIE8V/UTEilil1cSyG4eiBwLoLzMjECNYVA//79paIBftbIsIPgzsiCA9cqJkaAEWAEjI0AFA6mtHRo166dJti2sWVnfuZHgJUO5sece2QEGAFGoMYQQLRp7XS0NSZINTp+6623ZJyKarAwWlOkkHV1dZWfHTt2GI0vM2IEqoLA/PnzCWmqkWYYFg9Iv/zGG2/QvffeK7NUIaAsEyPACDACakKgT58+9PHHH6tJZJa1EgRY6VAJOJZ66/z58xxEzVInh+ViBCwcAaTcRMpOJuMgsHTpUrkTA3c3VjoYB1PmUnUEkLr61VdfJbj6YPfxyy+/JKTV/eKLL6hLly60e/fuqjPlFowAI8AIVIAAnjOmtnSooGsuVikCrHRQ4cStXLmStmzZokLJWWRGgBGoaQS6du1Kzz77bE2LYRX9x8TEUFZWFvXs2ZOCg4PljjIWekyMgLkRQPyGxYsXU6NGjejFF1+k119/XabRTEtLo2vXrlFERATHdjD3pHB/jEAtQMCU7hW1AL5aNURWOqhwui9dukS5ubkqlJxFZgQYgZpGwMfHh1q2bFnTYlSrf5hbbtq0qVo8jNEYVg4wZ3dycpLuFfCh/+uvv4zBmnkwAlVGAAqH6dOnEwJN9+rVi/744w+pcIiOjqaMjAxp/VBlptyAEWAEGIEaQgCBceEmxmQdCLDSQYXziIBleXl5KpScRWYEGAFLQODYsWOEHVC1EgJhYgw1TYisHRYWRomJiXJRN2TIEPr7779rWizuv5YjAMUiLB82bNhAjz/+OCHwak5ODiGey4oVK2o5Ojx8RoARMBYCpnavOHfunKrfVYyFs7XwYaWDCmcSu2mBgYEqlJxFZgQYAUtA4L333pMp9SxBFkNkqF+/PnXo0MGQpkZrg5SdiI3Rr18/wiLP3d1dE7TvyJEjRuuHGTEC1UFg4sSJtGTJEqlwaNKkCcG9iokRYAQYgeoiAIWDqQmpvvHbymQdCNhaxzBq1ygcHR2lKW/tGjWPlhFgBIyFAHwwsWD28vIyFkuz8kE6wJomuHfUq1ePWrduTchgkZmZSX5+fnJRh93kTp061bSI3D8joEEA383NmzdrrvmEEWAEGIHqIFBUVCQ3L0wZ06Fbt26ED5N1IMCWDiqcx6lTp6reJ1uFsLPIjIDVIDB+/HiCxRST4Qhs3LiRRo8eLRlAeaPsxgwbNkymKzScM7dkBBgBRoARYAQsGwEbGxtycHDgGHOWPU0WJR1bOljUdOgnjIeHh34VuRYjwAgwAuUg8MADD5RTykVVQUA7g5C2X+vgwYMJHyZGQI0IHD16lLCDiTSbSM+NhQXcMpgYAUaAESiLQGFhoVQ8lC3na0agPARY6VAeKlzGCKgFAWEmn3bsaClpPTqyWXcpQKzwIvfaVcpLSNCMzEnEOLD39dNcW/sJsldgUTR06FCLGCrMS83h32oRg2UhrBoBuAohGOqBAwdo27ZtBHdOVjpY9ZTz4BgBgxBQfvNM6V4BJSgC4P7nP/8xSEZuZFkIsNLBsuZDL2ngL4xAkr1799arPleybARiY2MpKCjIMCGLi+nSa6Ufxh237CSqa7jnVKaIFpx55rSUx8bFmfyGDjdMNiO0ilu1gtzatCOXpk2NwK16LLIvX6aUndsp6IGHqK4wKTSEjDWe+LVrKGnFzxoRgia9QoH3jdVc6zq5LMZib28vYxLoqmuJ96OiosjW1tZilA5xcXGUmppqiVCxTIxAlRHAs2Hr1q2l2v3+++/0yy+/UI8ePeiZZ54htrgsBQ9fMAKMgAkQwPvx8ePHTcCZWdYEAoavTGpCWu5TInDw4EH2obKi7wJSmj399NP07bffGjyqkNfeovbrt1L7Df9US+EAAbIjLlLq1s2UsPQHuv7j91WWqTg/n8LfeYvSjlY/gn/svM8p48hhvWTIFKbA6Dc/KVGv+lWtlLh+LcUvXkBZ4eFVbaqpX5XxaBqVcxLy9LMl873OsPSM7777rjShLoe1KoqQArCms1doA+Xv76/aoJza4+BzRgAI4Ddp2bJlGjBiYmJoxowZhKw3Fy9epDVr1mju8QkjwAjUXgS0XQtNgQKCNTs7O5uCNfOsAQRY6VADoFe3S+zw2dnZVZcNt7cQBP744w9pvvrrr7/SPffcQ/PmzaOdO4W1QhWorr0D1RVmsIbuwGt35X/3SGrx9f/IfaCBfunCHzhjz3bKi4nWZmvy84LUFNlvschHbwoKfvRxavTJHHJr2dIU7KvEs674+5fzLebcEMoXiqGrV68a0tQkbaZNm0a7d++mrKwsvfgje4WluFZAYOAJ31YmRsAaEGgpnnF5eXlSwYDxwNUCEeTbtWtHw4cPp3379lnDMHkMjAAjYOEIQKnx1FNPWbiULJ6+CLB7hb5IWVC9QYMGyTQ1FiSS0UQphrvApUuS38KFC+UxIyODPD09pc90bm6ujLqP9HQoQ5ArfOBTBkWMcg3FTF3hYgCT54CAAE05ynAP9bBIcHFxkedKGepDhpCQEFlPKQdvnBcUFMj+FT7p6enSzBQPRu0PBFeuITM0tco1jsp9BOxycnKi119/nV544QVav3693EXasGGDzK3evn17mijyrLu6uso2+v5XLPq89MG7srrffeMo88Rxyjx2hDz6D6LAMfdKa4hcoRSIXfQD5UYKU/ugYAp67AlybtRIZxeFYj4i3v4vkY0tObdsRT5DhpFzaKhsF7NsKWXetHBI/ONXSt9VojzxG3s/eXTuIuukHT5ECSt+o4LERHJq1ZpCnniKbN1LgqNmiV202B8XUKGYB98x+rsLRLw3lfJjYyT/K59+QjbOLvK8wWv/JXsfHxKTSnGrVlLajn/F2G3Ia9Ad5D+yJPOArFjJfyl791DS2tWaGs5NmpLdzVSTiVs2U+bpk+TcuAklb9xALsIVJGDsOLL385f1dY2nIixyrkRS9LfzyUcogLz69CUoUi7PmkFOou/g8RM0shh6MnDgQAoODja0uVHbpaSkUFJSEr311lvy76R79+4yK0THjh2N2o8pmeH5wIpgUyLMvM2NwCOPPEIvvvgitW3bllq0aEGzZ8+m7OxsaeqMNLFMjAAjULsRUN5lTYkCFJ1M1oMAKx1UOJdQOlgrQdGAqPAIFIeXeCgTlCA1WKBj0Y8XHygdoBxQPijHAxB1UQ9H3INSAIt6hQ/uKW2gdEAbXCv1UYY6ZcuVOlA8YAdI4QEZUR8+sEo75UGsHBVlBY5KGY74QJkARQf4KPeg0IAcCKCD3d8vv/xS8l+yZAl17txZv6kXvLGgT/zlJyoSCoii1GRyatqcYufPIXcRaBLKhYg3XxOL2Wxy7zuAMg/so8vvTqHWP/2qm/+NYnIWi+siMQfpYhGf+OtSartiPdkKJZBDcD26IfDJOnpAKjKcWpa8nNp6eEq++cnJdOmNl8ixcXMpX9qWjSTAp9BXXpP3L73xsjx63Dmc4pcv0S3LzRourdpIK4/ciPNinM3I5mZ/sAgAJW37V47duU0HIV8uRX8xi+z8A8ire4+bHCo+QCHi1LwF5QpFQPqOrVScn6epnHc1ilL+XElZ9RqQS4fOlLxutcQl9HWhlBFU2Xgqw8KpYSjZiJSWUTM+JJfFyylmyWIxR3sp5LlJmr6rc/Lcc89Vp7lR2yLd5Jw5cyhBBMb85ptv6MiRI3T48GH5d3bvvfcSFj9VVboZVUA9mOH5ofz96lGdqzACFo8ALImglMf3GkoHKCoRvLVTp06882jxs8cCMgKmRwDvqXgXZmIE9EWAlQ76IsX1zIIAXnDwAt9UBA788MMPzdJndTpRlBSwxkC+YuUhrJRDEYFzBO6DbxqulTLlPFksxLGoUspRH/w2b95MuAcfWiglQm9aE+gjL9ws6k14jFI3baDcC2ep/dqNVCwUM/lxsZQXf10s0O0pPzqKmv1vMbmIdGjIhnD2sQcJO+xY8FZGWITXf0YsWjE2YQ5/9rGHCDv+gePuJ5+BwgqnR0+KX/YjuffuS3DV0Ka0/XuFW4AzNRfuG1AIxAsFwfXvRSyLl16lHLGAL0xPpcafzyd3YeGRefYOuviifmZ1gQ88SCn794lYFBvJ/557yTGkvna3lL53NzmENqHmX34ty089eB+l79ujl9LBTezq4ZOye5dUOpRifPOi8fRZss8oRweSihT6r8SysvFUhgUCgdYXmJw9eoguvvyisOK4Rg3e/ZgcAgLL677CskRhTTJz5kyNUu3aNcFHxEPA3xmsfaDgUj4I2FRfZMFQrrXrQNEHXohir9zXbp8jLDFw7SYUJbivfQ/nsPbBywkUDNrt0Q5t0BfKlXST169flxH0161bRz/88IM064YlRCOhLANBKdm1a1caMmRIhWM35w1WOpgTbe7LlAjMmjVLw/7MmTOac/weIxYMlOdMjAAjwAjgWYDfd/z+mYrwDELq3jFjxpiqC+ZrRgT418OMYBurq1OnTskXeDY7MhaihvPBQxcfKBwqo7CwsMpuy3tY2MF3FunKEK0X1hxYhCF4V3Xm2rm1ME/DAlN8mn81X/YVf9NdIPL9t0vJlXn2jE6lg3QZWLSQso4dpuLcbNk+51JEKT4VXaQf2C/bnH38EVmlWFgdYGGeK5QhGSdPyDLXZs1Kjs2bV8SmyuWZh/aTW9+BmnZOwjIi8+B+zXV1TqBEUZQcth5ecjzgp2s8lWHhKCxGbMUuY8jLr1Pku/8l1y49yaf/AIPERJR5LOrxSUtLkwt/xVpHsSaCQgDKLsQmwAuEYsmDDnGOWAtwgQAP7fu4h2soFaA00+an8MB98MU9HBWeqA8LJUVJgf4V5QPK0B6E8h07dtC2bdso/GYQzytXrkjrH0tSOmCcTIyANSNQUwqH62l59Pz8ozSubwg91CfEmiHmsTECqkFAeR8w5W8f3j2w+cBKB9V8LSoVlJUOlcJjmTdXr15N8Hu2RsJDrLYRUpMhGvjZs2elAgPpUN944w1pyopd4OqSvbCwKEs2bu6yyHvEPSIeQWPNbSct5UhdO3u6UXR7cLwr0z8gG2Ht0Hj2XBGLwY0i3nhV016eCOUGqDA1TR61/7NxL+k3+NkXZCBE5Z69j690zcA1LDF0WVso7bSPNo5O8rIgLV0oAbTvEDk0bkb5165qCnHu2Lip5toUJ3A1AVU0nsqwkA3FIjZJZMwAZR7aS0jZ6aw1P/KGjv98fX1p6tSpt9WCe8VLL71ECBhnKQQlAxQRCFIHpdvevXtlsEvEV4FbkbassNZAvBNLotr47LIk/FkW60IgMSOP1h2Ko/UH4yghWSini25QVEI2zVhxnga29SN/dweq7+dMdja1753BumaaR6NmBEz9u4d3YFhTMFkHAqx0UOE8Ygccu47WSDCrhsl1baKvvvpKWjJMmTKFEDwPJujVpUIRyyJXpDmDJQECNmaeOyeDPSLjAci9Q0fp5pCyScRieHA8OYU2ErEIMsje10/TtUvrNpT4x3JK2PQXOQQGCZcMR3IVvr03xMLQxtWNbF1d5G4+TP/t/AM1i+K6Ir4FduaT/1wlFAnBZC9iJ9h5uEtrAK9+Ayhl3SpKEkEXfYbdJeMqiK1yGY/BrU1b2Xf0999RwAMPU7qIM1EVcm3VSo4pbvEPVDzuAaojMno4h4XKIJXuvfsQ0lUi0OUNsXOO2A9+oo4+lC0CmyINaO7NbA9ZwtSvICVVuqVU1l7XeCrDAnyvr1lFGft3C3eTryn6668o8sN3qMW3CyVWlfWrzz1YJsBtR3shr087U9ZZsGABIR0wXIqgfOjTp490owgNDb2t2/IUKbdVMmMB4lHguczECDAC1UOgsPgGLdhymdbtjxOBZR2oeeMgGtjTnQ6fjqaLCUXCqiqP9p67QPa2dcjF0ZbahbnTw33rU5BXyW9b9Xrn1owAI6AvArBwwGYBrHSNsUFWXr9Qajz55JPl3eIyFSLASgcVThoWCsZYmFri0JHvHsGrahOZIuc5TPthlg9C+kp8lPgNKEP2hUafzKarn8+ia7OmoUhS+/VbNRYIUEy4du6hue89eqxUOgQ++SzFzJsjY0AgToLX3WMoZf0qujb3C2r2+ZeST8DDj9DVT2dQ1LR35LX3qPuooYhRgAwW9V59U8ZxiNy3U95zat6aWsz/jqCsCHr+ZYr95gspr1u33kJhUBKAUlbU8R9iRAQ89ZzkfelwicICcRDgluB7x52UIbJQXP9BxI8Q5NajL3n17quDY8ntyI8/oLzIcE3dKx+8Jc9bLhFBN8tY5mhr/XWNpzIsEFsjZu5s8r3/ERHfogPZv/M+nZ34EF377htq8J+SYJsagQw4QWwGfVx+DGBd5SaHDh2iTz/9VCpSERV//Pjx1KtXL50uS1XuyIQNLD3QpQmHzqwZAaMhcC05h95ddo6SMwuob5dG1KKRr4Z38IBb7nY3hGLixMXrlJVTSBsPXqO/j1ynAKF0WPRSF019PmEEGAHTIoD3HbhCIli7qai5Ed1sTSUj89UfgTpCU8WOqPrjxTVNjMD3338vYxr88ssvJu7JStiLHeGjQ/qVGkzHLWIxL34I9CWk1yzMyiRb4XKBhXJZQopMLK5ty6TtRLmtMH1DgEpprYAAY9r9ikcLMjTUEWV2Iq5AqXuiE1hjFAurA3uR9UL7HiwRikQGDMQ0KBZHKVOZxX1ZGbWv0b4gNUW0cxBKixJ3DuV+kdDIQx7F4kMpN+VRn/FUhEVFcl0V6TQTf1+muR006RUKvG+s5lptJ7ByQKYKH6Q3VSFNmzZNWjp88sknKpSeRWYEah6Bq0k59MI3x8jXx52G9WlC9nb6mVSnCzeMDTvPk82NQpr2SBsKC3Cu+cGwBIxALUAAv9tKhjXEjmJiBHQhwJYOuhDi+2ZFADow7d1is3auxs6Er1vohzNLS6698C99p9wrLMDtb7pdlFcBioXySClX0lLeVkcoCuwrWUSWVQgo7evcDM6Ja2ThqCqhvbabiHZ7G2fzv5DqM56KsNCWXfvcZ+hwcm0rAoTeJO1YHEqZmo5PP/10lcSFZUSHDh3ozjvvrFI7U1WGiSk/t0yFLvO1dgTyCorpraVnyM/Pg0b2LwkkrO+Y3d0caNSglrRq80la9E8kffBQK32bcj1GgBGwcAQiIiIoLi6OevfubeGSsnj6IMBKB31QsrA6McJXH4tzpGC0NsLLu305u+3WNk5jjsdLxCtgql0IIKBkVYNKWhNCeAYiSKalECtLLWUmWA41IvDhb+coO/8G3TesagoHZazOjnY0oHtT+mfvedp3IZl6NPNWbvGREWAETIyAKQ3mocyHBTQrHUw8iWZir78NtpkE4m50I7B8+XI6cuSI7ooqrIEotUpaPRWKzyIzAqpAYPLkyYRYCmoluGEgq4WlkClfuixljCwHI2AKBFbsjabjl9JoRP8W1WJfL8BNBJ50pFWCHxMjwAiYHgFzWPchQDNSazNZBwKsdFDhPCaKbATR0fzDqsKpY5EZAYtAAD/kp0+ftghZDBHi4YcfpsZaqV4N4WHMNlA6IKAWEyPACFQNgRV7YyisgS95eThWrWE5tQd0bUxnr2ZSrnDXYGIEGAHTIwDFgymV7u4iLteIESNMPxDuwSwIsHuFWWA2bifY4QsKCjIuUwviZg7tqQUNl0VhBMyOAII+qTkqdKNGjcyOWWUd4qWLn1uVIcT3GIHbEdh6Ip4ycoro/u5ht980oMTHy4kcHWzpn5PxdFenQAM4cBNGgBGwJARCRcpsfJisAwFWOqhwHl988UUVSq2fyJcvX6bU1FT9KnMtRoARMAiBWbNmGdSOG5WPQHx8PFs6lA8NlzICFSKw7VQihdbzqvC+ITe8vVzo9NV0VjoYAh63YQSqgAAsJuEObUpLhyqIw1VVgADbg6pgkmqTiNBoenkZ9yWkNuHHY2UEagMCmzZtkql1LWWs3t7eIvK+n6WIw3IwAqpAIDIui0ICRMpkI5LMjSRWAABAAElEQVSriyNdS8w1IkdmxQgwAuUh4CYymzkYkGGsPF4VleXk5FC2SHXOZB0IsNLBOubRakYBramLi4vVjIcHwggwAsZH4MCBA8ZnWg2OCHRlZ2dXDQ7clBGofQhk5RWSu2vV0yJXhpSdrQhGXXijsip8jxFgBIyEAIK/JycnG4nb7WxSUlLoqaeeuv0Gl6gSAVY6qHDa5s6dS7///rsKJdctMl7codlkYgQYAdMh8M4779D27dtN14GJOSO1Lj6WQoiRwUoHS5kNlkMtCEBBkJOXb1RxCwuLyM6mjlF5MjNGgBEoHwH8DpvSOrmwsJCKiorK75xLVYcAx3RQ3ZSRjDofFRVF48aNU6H0lYvMAdkqx4fvMgLGQCAhIYGOHz9O/fv3NwY7s/Po3LkzYYfFUmjq1KkUHh5OO3bs0Iiky8+17LMuLy+P7O3tZXtdbZWI4efPny83IGhl7XEPFmVKX+gQO1VwEQFpt9U+lzfL3FfKcCyv7rFjx6h9+/ba1TTn5dXX3CznRLt+XFycDKasXaY0Ka8M4y1PKVReXe2ykydPUtu2bcsdG/rTrqv0r8yNcq0cYQ1ja3v7Kxd4XL9+nQICApSq5fLFzfL6q2pZUlISIeWsQoqVTkVyV9RvReXlyVNh3Tw/ik3MokYhJd89RabqHNMy8qgwLY6WLDkm2VRJHjEX5ZE2DyyA8OzBYkiZT+37SvvyynBPKdcOPKuUKW2VY9ly9FnRc69sXaUvZDqrV6+eDHRbXp2K+lLKsajUllUpV47aPMvW076n1Mexor8F7ToKziiriI8x7oG3IjfiiXl63nL3qaxfbVnLnpdtp92H0pfSBvhqZz4q21app33Uxka7XJ+22vVhRaCtMCgrS1lZtdviHM/33NzcSuenbJuqXmOsvXr1qmozrm+hCNz+C2ihgrJYtxBo165dqZeGW3f4jBFgBBgB3QgEBgbKxZTumpZZw1JTaH3//feERV1VCS93zs7Oevmuar8IIpCXq6trVbuTL7l4wVQIiwBtJYRSjqN2f7iuyostFvu7d+++jQf46Etl+0c7WMM5Ot6eYrG8uqgPv2ModSqiitqlpaXRoUOHKmomyytqW7aR9sJDuae0xXicnJyUYr2OSlu9KmtVAg7a2Gl/77TnVpu/drkWK82pdl2lsLwy5Z5yzPDuTlHOftS7Q32lqNrHxNQsKo46Tut1zBs60kfGsgLBjz0jI0NaWmkvFsvWK3tdlb7K4q20hUUVvpNVIch66dKlKv3dluWv9F+23NBrPHsqUp6AJ8aPxX9FQcWNLQ/6BM+srCyTuPci7WN6ejq6uY0wVn3Ho9TT9Ty7rZMKChArAX//CkGJBsVWVQh/A2W/r1Vpr6tuWFgYvfTSS7qq8X2VIMBKB5VMlLaYkyZN0r60qvOqPICtauA8GEbAjAi8//77ZuytdnTVpEkTsbu6pHYMlkfJCBgBgZSsAnpg5n5KTskhb5HusroUn5RNmZm5tP7LycLFgr2Hq4snt2cEKkPghx9+oD179lRWhe8xAqUQ4KdyKTj4oqYRgCZX0ebWtCzcPyPACFgmAleuXCG4mDExAoyAehHwcrGjzk29aMeRK0YZxO5jUdShiTcrHIyCJjNhBCpH4IknnqBp06aZ1NKhcgn4rtoQYKWD2mbMyuWNiIgwaSRcK4ePh8cI1AoEFi1aJGMo1IrB8iAZAStG4LUxTSghKZ1OXoyv1iiPnImllNRMen5YaLX4cGNGgBHQHwG4avr7++vfoIo14a54//33V7EVV7dUBFjpYKkzU4lc3333Hf3000+V1FDvrdDQUPL19VXvAFhyRkAFCEyfPp02bdqkAknLFxEvImzpUD42XMoIqAkBH5Ey84khobTncCRdiyvf713XeM5fTqRj52Lpvr4hFOZ/y0ddVzu+zwgwApaNAIJdKkGOLVtSlk4fBFjpoA9KFlYHQYEQtdwaid0rrHFWeUyWhkB8fLyqLQUaNmxIDRo0sDRYWR5GgBEwAIH7egTTiB4BtG7bOTodUTWLhwMno2mnUFgM6hhAj/ULMaB3bsIIMAKWigAy7SDQMZN1IMCBJFU4j02bNi03/ZcKh8IiMwKMQA0gEBQURIgKrVaaPHmyWkVnuRkBRqAcBF4Y1ph83R1p8ZbLFBGVQoO6hZGrS0kK2XKq06WrKXTgVDRlZuXSvX1D6Yn+geVV4zJGgBFQMQJI6blw4UIVj4BF10agjsgWUH5iYu1afG5RCEDrhzQ1laUcsiiBqyDM/Pnz6ciRI4TUc0yMACNgGgRiY2PlMyQgIMA0HTBXRoARYAQMQOB6Wh7NXhNORy+mkK+PGwX4uJK7UD7gVTUzp4CS03IoMTlTcL5B3Vv60uSRjcjFwcaAnrgJI8AIMAKMgDkRYEsHc6JtpL7s7OyMxMky2XD2CsucF5bKehCApQMTI8AIMAKWhkCAhwPNerQ1xabk0uoDMXTqSgZFJBZKMd2cbKlZoBM90rcx3dnOz9JEZ3kYAUaAEWAEKkGAlQ6VgMO3zI8AdjNY6WB+3LlHRkBNCLz//vs0YMAA+VGT3CwrI8AI6IdAkJcjPT+0kX6VuRYjwAhYJQJZWVkEd8pvv/3WKsdX2wbFgSRVOONLly61WvcDVjqo8AvJIqsOgTVr1hA+aqWYmBg6ceKEWsVnuRkBRoARYAQYAUZABwJQOhQXF+uoxbfVggArHdQyU1pyXrt2jfCxRmIrB2ucVR6TpSEQERGh6ojQcA9p166dpcHK8jACjAAjwAgwAoyAkRBwdXXl9NhGwtIS2LB7hSXMQhVlqF+/PuXk5FSxlTqqJycnq3oxpA6UWcrajgCC0CIYrVrpgw8+UKvoLDcjwAgwAowAI8AI6IGAs7Mzbdy4UY+aXEUNCLDSQQ2zVEbGu+++22rjHqSmplJGRkaZEfMlI8AIGBOBZs2aWe0zxJg4MS9GgBFgBBgBRoARYAQYgeojwEqH6mNodg7IW2utFBoaStnZ2dY6PB4XI2ARCAwfPtwi5GAhGAFGgBFgBBgBRoARYASsHwH12tda/9zU2hGq2ey71k4aD5wRMCMC77zzDm3fvt2MPXJXjAAjwAgwAowAI2BOBAoLC2nu3Lnm7JL7MiECrHQwIbimYr1q1SpauXKlqdjXKF9kr2BiBBgB0yJw/fp1io2NNW0nJuSekJBAJ0+eNGEPzJoRYAQYAUaAEWAEahKBoqIi2rFjR02KwH0bEQFWOhgRTHOxys3NpejoaHN1Z9Z+kBqHLR3MCjl3VgsRgNJSzZYCfn5+nL2iFn5veciMACPACDACtQcBOzs7SkxMrD0DtvKRckwHFU4w/gitlThlprXOLI/LkhBAyl0EbVUrffTRR2oVneVmBKwagY1H4yghPZ8m9G9g1ePUZ3CMhT4ocR1GoGIEsAn5xRdfVFyB76gKAVY6qGq6SoRF3trmzZurUHLdIsO9ghUPunHiGoxAdRBo2LAhubi4VIcFt2UEGIEaQGDRv1fouz8jND23DPOge3oG08guQZqymjyZs+oipQqlw4N96pOdTR2NKD/vukYHLiTRnCfaa8rUfKLPeCrCQs3jZtkZAXMj0L69dTwzzI2bJfbHSgdLnBUdMg0bNkxHDfXehq95ZmamegfAkjMCKkDgmWeeUYGULCIjwAiURSCvoIiKim/Q6w+0oNz8Ilq7P5am/XSGOjbypBBvp7LVzX4977mOlJ5TUErhACEuxmTSwTPJZpfHVB3qM56KsDCVTMyXEWAEGAFLRoCVDpY8O7VQNsR0QLRaJkaAEWAEKkJgxowZ1KdPH+rbt29FVbicEbBaBGzq1qGxPevJ8Y3qFkx3TtlOK/dG0//d3YQQi/mHfyJp06HrlF9YRIM6+NPzwxrfpgQoC84PWyPpamIOvX5PM3p2/hF6emgYFRYV049/X6GF/+lMH/xylqISsqlXKx8K8HCg5duuUlthZfHCXY3I182BPv/zIh0NL3HZ8nG3p86NS1J7RyXl0NtLTlFUXBYVFBbThDkHZdeernY09+kO8jwrr4g+W32BjlxMIRdHW3p4QH0aoaflxvKdV2nl7mhytLehh/qH0C87rtEnj7WhekIB8/qik9Sivhs9eUeo7AfXrRq40eODSq4vx2fTZ6vOU6SQLdjHmV4Z3YRa1XeXdXPzi2n+xgg6cD6F0rLyyVZgPrpXMA3pGKhzPBVhIRmL/7Ycv06Lt0ZRRnYBdWrmRa+PbkbODjZUUHSDnvjqEI0Rlisbj1yn9KxCeqBfCI3pHqw05SMjUOsQ2Lt3L3Xt2pVsbXnJqvbJ50CSKpzB8PBwunjxogol1y2yv78/eXt7667INRgBRqDWInDlyhXCc5CJEajtCMCqAOTmVPJCvvZQrHS/cBSL2ECx8F625QqtE2WgVLF4jknJve2TIsoTM/Jpx4kEuiIUCxeupNOh8BQ6HplGEVczyN62LjULcaPk9Dxaty+G5q4Ol9cb9sbQamFpAWro60xtQj0oVfA5eiFFluE/R9G2Q2NP8hKKCBDO8WnTsGRxj7Jpv50j8GoU7CIUJcX0kbDciEvNxa1K6eSVNPpyxQWpzAj2caRZv52XsmflFMl2UGKcv5ah4YHrC9FZmutnxAL/1KU0atfIiy7FZtJ/vj2muTd9xTn69Z8ocne2pf7t/Kh/e39qFOCi13gqwgLMMa6pP56iaKHw8BXKmw17YuizNSXvc3AvBfaf/3GBSCiPoPT55Geh7BGKGyZGoLYiMHXq1No6dKsbN6uNVDilZ86ckUqHyZMnq1B63SJzTAfdGHENRqA6CCxcuJAQkPbRRx+tDpsaa+vj40NNmzatsf65Y0agJhGAe8Uzwhohv6BYLlJh+TCwrb8UafOROEKch0UvdZHXk749SpvErjl2y1/+/gSdvZx2m+jdWvtQj+Y+lJNTSJHxWeTqYkeXr4ujox35eTvK+o8NaECnhBJix7F4+v6VLtS2oQe9kl9IaWK3HnTfTcuLTMFjx/F4WYb//MXCevKoppSVW0SbU2LlueamOIFlxk5R/4m7wujZIY3kdd/X/6VNoh/0WRltP5NIGPvKt3rK44yV52m1sHTQhy4J64b0zALNWMJjs2j8zH10QbiBNAt2pfjUPMnz8cGh1KWJVylLkcrGg74rwgL3ttzEZsXUnuTtak8vLzxOO08IvO5vgduS2jf1pK+f7UjJmfk0fOpO+vdkgk4slLZ8ZASsDQGsCfLzhbURWzqofmpZ6aDCKYyLi6PkZOvxjdSeAg4kqY0GnzMCpkHg2LFjMjWtWpUOL7/8sliciNUKEyNQSxGA9UFmdqGM7/DehNYU6ucskTgq3AFAd7xdktseioS6YmEOmibqoU1Z8hBKhrPX0iWvs9EZ1LuNL524lEo+7g7UIKCEr3abljddEIwRFBKLfLhdLN18hX7bXqIwwPXBC8k6F9r7zyVTiLA+gOIB1EVYUFSmdCgW7gsKbT+TIE9f/u64UiSPB4WFB5QOcBt5dcFxevmbo7K8cwtvmvpASwr2KlHClGpUhYvdZ5KkUgcKB1AHEYtj78lEaQGhlHUU4wAp15k3rVlkIf/HCNQyBLDBamNjU8tGbZ3DZaWDCuc1KCiIiopKzAdVKL5OkdnSQSdEXIERqBYCnTp1UnWWGD8/v2qNnxszAmpGAIvsec90kDEABr+1nX7ZeY3u6hwoh+Qk3CywBn9Ta+fcScQ7AMlAkxV4L6bfVEYcuZhKj93RgLYLSwPo9fo3LP235i5iMSC+QVXJyb6uVC7AfQIKE4U8XUpeQ9uIhfa43vWUYgrSY3Hf0N+FdgsrgIoIYhYIFwVQtogbkZ1bqFFWerqULPrvEX22Eq4jCikxHWDJsWVaP2n5sPNsIi1cf4nmCDeITye2lVUrGo/Cp6Jj8xBXOno+WbqRAIfLcdlSaeInFDywYAEpSpSKeHA5I1CbEBg+fHhtGq5Vj/XWk9+qh2ldgxs5ciQ9//zz1jUordGw0kELDD5lBEyAwOOPP04TJ040AWdmyQgwAuZCACkpHxnckM4Lt4ejwjIBNLhzgHQb2HU2iWxt6oqYBHYU6Kl7dz7ophvFJREDoUmQGyEmREJyLoWKhX2hWAwfEfwTRSpMuHTgPD4tTzNM5T7Kr4uYBUXCogDnp6LSNXXuaFfi/vHhr+don7Bi2HY6USo1AoRsQX5OdALxFkRcBTcnO3Kws6Hgm/JoGJRz0r+Nj1QkfLU+XPJcsCmyVK12TTxlxgzEtPhIxI0ARYv4CIhr0aelj1zcI0bFdTEWWBVgsa8oO3YKiwRgmC7cRzwFhqBYgYdCFY1HFxZ3tg+QLF4TQS0Xb4uirYfjCO4trGhQkOUjI8AIWCsCbOlgrTOr0nGxe4VKJ47FZgTMiMCyZcsoLCyMevXqZcZeuStGwPIQGN+vAf3412WaszaclrzchV4ZIeIniECKCMyID2hUn3r09thbMQPKG4W7sJDAwhe77fV9naiBUDacSE+hMH9ngnn/818d1jTDuRKDAYVYmGvfRxmu7cRO/q7PBuKSOoZ5ysX1loOxhA9o3Yd9CDv8Xz/fkd5cfIoWiXEsosvy3own29Kgm3EqZEE5//Vt6SfjVyBYJj6NtSwWUH3iwIZ0TgRmRGDKjs29KVCMC4EafxEZL14VcSY+f64DvffTaRmMUmH/78wBMpPEJ7+fpcSUW4oVTxEIc6qW9UhF4wGGlWHRpoE73dk1SGKw/1Qige+k4Y2V7vnICDACZRCAOyhiOLm4uJS5w5dqQ6COWOTdcnJTm/S1VN709HRpIujh4WF1CLzyyiuUlpZGP/zwg9WNjQfECDACxkEAz4mhQ4fSsGHDjMOQuTACVoYA3uwSRLYJG2EN4XMzfoAlDBFuDgg+CYsGV8fSftpIGZmUkUcuDraabBz6yJwpglTa29YRlgmJNGXhSVr6endqVs9V0zRDxLVAdo+c/CKpWNF270AlyIQsIJ7O9iL1ZokBMJQvCORYKGTyEJYOSGlZHlU2nvLqK2UYa2ZuAXnddPNQyvnICDACpRG4++676ZdffiE3t1tuUKVr8JVaEGBLB7XMlJace/bsoaNHj9KUKVO0Sq3jtLi4mPBhYgQYAdMhsHTpUsrNzaWnn37adJ2YkLODgwNZo9LVhJAx61qGgAj4LjNHWNqwsXivaAEPdxF9XEHKjqms8qLsfSWdqBLbouz98mSCxQKsMHRReW11tcF9jJUVDvogxXVqOwLIWoHNVlY6qP+bwEoHFc4h8tPHxpaYJ6pQ/EpFDg4OJkdH3f6nlTLhm4wAI1ApAkeOiHR7IgWVWun+++/XBIRT6xhYbkaAETAuArCeQKBLOzsOV2ZcZJkbI1BzCIwdO5ZcXW9ZLtWcJNxzdRFgpUN1EayB9vBtwi6lNRJ7+1jjrPKYLA2BLl26UFZWlqWJpbc8yL7BxAgwAoyANgJdm3jJjBPaZXzOCDAC6kZgwoQJ6h4AS69BgJUOGijUcwJfZnyskTiQpDXOKo/J0hAYP368pYnE8jACjICVIoD4CJHx2dQ4sGYDwUWJzBWBHg6lUnZaKeQ8LEaAEWAELA4BVjpY3JTUboFY6VC7559Hzwjog8D27dsJcR169OihT3WuwwgwAjWAAIIlfvDLWZmpATES9nw+yOhSfP/3ZeraxJvah+oOrP3a9yfoikjL2aqRJ80R2TE8OYij0eeDGTICxkYgNTWV6oggNRzHydjImp8fO76ZH3PusRIE8GDBh4kRYAQYgYoQWL9+fUW3uJwRYAQsBIHJP56QCocJQ0Np2Zu3FIR3vL2D+rz2r8waAVG/3XSJRn642yCpF6y7JLNW6NP46+c70OsPtKDzkWn00KcHSBhgMDECjICFIzB58mSC4oFJ/Qiw0kGFc/jPP//QBx98oELJdYvMlg66MeIajEB1EUD6qW+++aa6bGqsfWFhIRUUFNRY/9wxI8AIVI7A5etZtP9UIo2/syG9OLwxhfk7axrkiRSVBYXFtPpAjCzLKyimXJHO0tSEbBRje9ajjya2oeTUPNp4JM7UXTJ/RoARqCYCdnZ2VLcuL1erCaNFNGf3CouYhqoJcfLkSavNXnH58mVeTFTt68C1GYEqI3DlyhVKS0urcjtLaTBgwACyt7e3FHFYDkaAESiDwJYT8bJkwoAGZe6UXNrZ1qWf/42ih/vWv+3+luPXafHWKMrILqBOzbzo9dHNNGk2jwsrhdmrL1JaZj49MqjhbW23nU6kH7ZEUlJaLrUWLhf/va85+biWflYMautPzo62tPHodbqrc+BtPLiAEWAELAeBZs2aEdJmMqkfAZ5FFc5h+/btKTk5WYWS6xbZ39+f8vLydFfkGowAI2AwAmFhYZSQkGBw+5puOGrUqJoWgftnBBiBShC4lphDjg425FVB3ISh3YNo3e5oghKh+MYtP4e41Fya+uMpqRRoVM+VNuyJIeF0Se/e30L29sK8I/I4qHMAfb/xUikJEtLz6L8LjpOvl4NUOOw6niBS6xJ9OrFtqXrw4AwNdqW4JOvMAlZqsHzBCKgcgVdffVXlI2DxFQRY6aAgoaIjdvnwsUZydHTkmA7WOLE8JotCAM8Pjp1iUVPCwjACVoVAklAAuDpX/IpZz9tRBnT8aVsUBfs4asa+5XiJhcSKqT3JW1govLzwOO2E1YRQOlyIzpRuGXOe60C9WvjQ4YgUmjS3RAkBBpuPxRMCVq56u5fMULF0exT9788IGbtBFJcib3c7uhav3rTBpQbDF4wAI8AIqACBin8RVCA8i2idCPBiyDrnlUdlOQjAokjNFB4eLpUmjRs3VvMwWHZGwGoR8HV3pFOXKnfhmjCwPk1ZeJLu6hWswWH3mSRydbGTCgcUdhCZJvaeTCRYQOy9WGLh2T7MU9bv2MhL0w4n208mENJzDn9vlywvFHEjEDviilAuhAWUTteZnF5AXm4OpdrzBSPACDACjIDpEGClg+mwZc4GIMCBJA0AjZswArUMAQTBfOihh2rZqHm4jIB6EKjn60i5ImBkalZ+hakpB7Qpia2w5UAcOTnayME1D3Glo+eTKV8oC+xF3IfLcdnSegFBIEP9SoJRRiflUDPhHoH3BW3yEMoK0CtjmpKTfQk/XAd63bKkwDWaRYlAl63DdKfZRH0mRoARqDkEZs+eTSNHjiTEdmBSNwJ11S1+7ZR+586d9O6771rl4FnpYJXTyoOyMATWrl1La9assTCp9BcnMzOTYmJKIt/r34prMgKMgLkQGPz/7J0HfBRV18Yf0ntIQhqhJHQIHaRaqCoKKCKIYuFVsZfX7mcv2LAgCryKCCqoiFIUkCYgvUvvLZT0Xknnu88Ns24C6QnZDefkt9nZO7f+Z3Z27plzzlXBGik/rj1TbJN0eRh+XQNtjWBkGtjBX28+/91efK9cL1buiEK3UB+teOje3Fvv+2juYWw+koDxC44YxfT7zVcF6vffVKyIc2o1DB93B3gpFw1zBQQzrDkQh7T0HFzf0botvvRg5Z8QqOUEjhw5gsTExFo+yitjeKJ0sMLjfOjQIZw9e9YKe162Lot7Rdk4SS4hUFECmZmZCAsLq2jxGi/XsWNHBAUF1Xg/pANCQAhcmkCTAFd0be2NH5aFYfrKMDDI46Vk1NUNCiW3beSBgUp5wOU2p6hVKlxVXIjH1JKbFCcHG4wZFIJ9x5Pw9JSdOHAqBVwFw5A+ofXw+K3NcexMKt6ddQAPT9yBt386YOxGyrlc/KmUGK98uwd1PRwwqLOsXGGCIxtCwEIJcE6Qn59vob2TbpWHgLhXlIeWheQNDQ3FiROFozZbSNcq3Q1eWGQ93kpjlAqEQIkEbG1trTqQ5KOPPlri+GSnEBACNU9gwgMd8PLMfTqY47TFJ7Dxs366U+s/6WvqXD0VV2HL5/1Nn7kxbnQbvDmqNdIyVdyFIqtfPHpDEzw4IERbMng42+l3J/t/XSnuVUt08pWgltRkfAcGozTkwS924FRkGkKC3DHpkQ7aesLYJ+9CQAhYJgGuaOfvX2ABZZk9lF6VlYAoHcpKyoLycclMPukTEQJCQAhUhICjoyOaNGlSkaIWU4bWGlztRkQICAHLJMCYDJ/9pz0ys/NxNDK1XJ20t61zkcLBqID77JXCgVLUdcLIY65sMNLevTsUgWrVDCorRISAELAOAklJSRfFb7GOnksvixKQK29RIlbw2dW1cBRmK+hymbtItxGxdCgzLskoBCpEYPDgwRUqZymFli1bhm3btuG1116zlC5JP4SAECiGAN0i2jWu+aCNLYPciumhJAsBIWCJBNLT03HfffdBVqqyxKNT/j6J0qH8zKRENRIIDAwEn8KKCAEhIASKI0A3rJMnTxa3W9KFgBAQAkJACAgBKyfAh6y33nqrlY9Cum8Q+DcCj5Ei7xZPYMuWLfj8888tvp8V6SADxkggyYqQkzJCoOwEjh49CkaEtlYZNGgQfH19sWPHDmsdgvRbCAgBISAEhIAQEAJXDAGxdLDCQx0eHo4DB/6NyGyFQyi2y7JkZrFoZIcQqDIC+/fv15YC1rzu9YcfflhlPKQiISAEhIAQEAJCwHIIxMbG4p9//sENN9xgOZ2SnlSKgFg6VApfzRRu2LAhPDw8aqbxam6VSgcRISAEqpdAQkICUlPLF9itensktQsBISAEhIAQEAJCoIDAyy+/XGtX6rtSj7FYOljhka9Xrx6eeOIJK+x56V0WpUPpjCSHEKgsAR8fH9jb21e2Goso//PPPyMgIAB9+/67DJ9FdEw6IQSEgBAQAkJACJSbAH/X3dzccMstt5S7rBSwXAKidLDcY1Nsz0JCQordVxt2SEyH2nAUZQyWTKA2/ZAHBQVh1qxZCAsLw+jRo+Hg4GDJ6KVvQkAICAEhIASEQAkE7rzzTgwfPlx+z0tgZI27ROlgjUetFvc5Ly8PjEwvIgSEgBAoC4Frr70WTZo0wcqVK003KFxmKyYmBo0aNYKtrW2x1fBak5OTo/PQysqw/sjMzMS5c+f0SjpMN5YppksKPycnJ8Pd3R1169bVdfO6xbXE4+Li4O3trYNcGo1GRkYiOjoafn5+qF+/vpGMM2fOgEsEM43tGvtOnDih23ZyctJtNWvWTJc5fPiwfmd9/v7+aN26tf6cnZ0NBgY9duyY5tCuXTtTG7t27QLLMXZHp06dTOnbt2/HwYMH0apVK12Gli+UTZs2IS0tTY+Nyt/u3bvr9K1bt4LtcPx8+nTNNdfo9MTEROzevVu30bJlS/Tp00en89+qVavA9tu3b48BAwaY0lesWGFK79+/P+zsCm5Dli5diqioKD02Lo9mxBvh8qi5ubmaBfPeeOONui6OmWNgGVq6DB06VKcz76JFi7Bv3z6EhoZi2LBhprbnzp0Ljr1r1676htbY8euvv2qGZM0gpTy2lDlz5oBj5HEmqw4dOuj0NWvWICIiQveLro6GEo+Kr40bN4LHiH2iEswQKsYYS6VNmza45557jGT88MMPOiBqly5dcO+99xZK5wotfMjA6O2GS+X333+vzzVPT0907txZ82Wh1atX49SpU/p8ZttDhgzRdfF8Yn/pH83gq//5z390Os9Z1sWAsmR9//3363T+mz59OnjMu3XrVij922+/1ZyaN2+ux2Ao+JgeHx8PnkfXXXcdjHOWx4EsuAw2v6OGNdKePXv0krcpKSnw8vLCmDFjdNtkPXPmTH2ukdNNN92k0/mPfeL51LFjx0J9mjFjhj4P+H0wxsb8TOf3kZahPF+NPvF8Wr58ue4rz5m2bdsyuzbjXrdunb5u8LvKZfoM4THiseP5VPQY8fxnn8yPKY81Y29RIdqvXz/Td5vnH79D5MZ9ZEVhHYzTxesKz7VRo0bpdF7HyJDnOo/RyJEjdTr/8dzkssE8RiNGjDCl//bbb/qY8vtIX3h+XymLFy/Gzp079eebb74ZPIaUv//+W7fL7zdXDjO+R7w28TvM84SMrrrqKp2f/4y6eP6ZH6MlS5boNphufE+Zn8x5bgYHB+u6jGsd0zZv3qyPExn27t2b2bUfP1nwxfPD4MRrLM8dfu95fhjXIZbZsGGDZshj1KtXLyZpYf1ky/w9evQwkjU7jpHuyrxm8/ykMIYAfxeM3w3jusk6+FvB3xVef3ldo2RkZOj4SPye8brFdgxhP3lNYN08HobwWn369Gk0btxYt298j/h9N/rEvORFYV6KEWi9QYMG+jOvQRR+t3nOcvU3Cn+/+N2iGyWF30t+9w3h7xGF9bGcISzDc47XP/bJ6FdWVpZO52eeI8bvJM8ZsjKu4ca7+T08v/ulCTnxnCLb8ePHm7Ib7ZsSZMPqCYjSwQoPIX84eDHhDVttE14EjYt9bRubjEcIWAoBTngpvOGqDcKbMPNJAm/qPv74Yz155xh5s8pJPG9q5s2bZxoyb3B4M8kJAic7xs0wJ8D/+9//9E3ZSy+9ZJos8YbwxRdfhLOzs0579NFHdV28bo0dO1bfvHJy+Nlnn5naeO211/RNGW/oaDJqyE8//aQnxixrHhSTN9Xz58/X10FOcIzJEifTEydO1De5VLQYSgfWN3nyZD1R4w06x20IJ2q8MaQywbh55r4///wTx48f15Od//u//zOy64kBb9J5Y2k+WWcfOaHgzSwnFIZQAcOJCW9iOfk3VzpwAscJJydZ5koHrr7Em2j2y/w3jDftTGc9Rddk5zGiGBNEbrNPnDBzosrJgLnw5p3H2phwGft4nJmf7+bCNnmOsAzHZAgnFEznxMs83VA88ZgYCgqWYZ84XiqsePNuLkzjzTsZmgs5sB6+mwvHxHb5XrRtfjZPM8oZaca7kc5+GS8jje/8reWkoOhvbnHpnGxwYmFMOoy6+JlliqZzP/MXrZ/9Y16+ik5KmJcv9tdcmHapvhr5jHejTNHPRjrfOSliH8w5cZu8uY/v5sJjcKljxGPGV9FjyvOGaTzmRcVQOhTtnzHJNp9osU9U5nAfJ5bmwnOVdfHcNRd+51iG57h5G8xn1FG0X7wWcHyGIsKoj9dRnt+sy1zpQAUMv6s8fuZKByqFqEhgv82VDkznBJl9Nv8OM437eD9LpYO5UFFGjub5WS8VRqyH4zFXOnDSynp4HTFXOvBayu/8oUOHtBLVYMLr3N69e/X4Hn/8cVPTHOtff/2l2RmKO+5k21Su8XtN5YWhdGA6r7/8blPZ995775nq4ipzbI/n1NSpU03pX3/9tem6wm1DeI2lEoi/L6+++qqRrBVeP/74o+4Df+cMpQPPi7ffflufrwMHDsRDDz2ky7BPHBMVkxQqFt5//329zX+ffvqpVkrwt8p8kj9u3DjNkGzJzRCOm6xY73PPPWc6T8iJdVEYf4F9oJA126eyg+fBgw8+qNPJbvDgwVqRyt9atkehIppjMcrrRPlXKwnUUSfRv7+wtXKItW9QVDp89913+ga0to2ON/i8ceYFTEQICIHqIfD777/rJ+O8gajtYkwYeePHCVDRSU5tH7+MTwgIASEgBIRATRMwlHlU4vG3mC+RK4uAHHErPN7GkxEr7HqpXaaGnhp3ESEgBKqPAJ9k8Lt2JUjRp6xXwphljEJACAgBISAELImA8VtsvFtS36Qvl4eAKB0uD+cqbYVmwrXVDIlWDvTBFBECQqD6CND3s6g5d/W1JjULASEgBISAEBACQkAIXMkEROlghUefwayMgFZW2P1Su2z43JWaUTIIASFQIQJGsLAKFZZCQkAICAEhIASEgBAQAkKgHARKDytajsokqxCoLAEG4yka5KiydUp5ISAEhIAQEAJCQAgIASEgBISAEKgZAqJ0qBnulWqV0YG5DE9tFEZuZuReESEgBKqPAAM6GUGdqq8VqVkICAEhIASEgBAQAkJACACidLDCs4DLJJkvi2aFQyi2y7KYSrFoZIcQqDICXDJxwoQJVVafVCQEhIAQEAJCQAgIASEgBIojIDEdiiNjwelcH5mKh9ooVDrIkna18cjKmCyJwJ49e3D27FlL6pL0RQgIASEgBISAEBACQqCWEhClgxUe2ODgYPTs2dMKey5dFgJCwBIIMBAtXZlEhIAQEAJCQAgIASEgBIRAdRMQpUN1E66G+tu0aQO+aqPk5+dDVq+ojUdWxmRJBAYPHgy+RISAEBACQkAICAEhIASEQHUTEKVDdROW+stFICEhQZQO5SImmYWAEBACQkAICAEhIASEgBAQApZLQAJJWu6xKbZn6enpSEtLK3a/Ne/w9vZGUFCQNQ9B+i4EhIAQEAJCQAgIASEgBISAEBACFwiI0sEKT4UzZ87gueees8Kel95lBpIU94rSOUkOIVAZAosWLaq1K+BUhouUFQJCQAgIASEgBISAEKh6AuJeUfVMq73GU6dOIS8vr9rbkQaEgBConQR27dqF8PDw2jk4GZUQEAJCQAgIASEgBISARREQpYNFHY6ydSYkJAQeHh5ly2xlubgyR5cuXays19JdIWBdBDp27AhHR0fr6rT0VggIASEgBISAEBACQsAqCdRR5uznrbLn0mkhIASEgBCoEIGsrCztxiTLZlYInxQSAkJACAgBISAEhIAQKAcBUTqUA5ZkFQJCQAgIASEgBISAEBACQkAICAEhIATKTkACSZadleQUAkJACAgBISAEhIAQEAJCQAgIASEgBMpBQJQO5YBlKVmPHz+OsWPHWkp3pB9CQAhYGQGuXvHbb79ZWa+lu0JACAgBISAEhIAQEALWSECUDlZ41Lhkpo2NHDorPHTSZSFgEQSSk5Oxb98+i+jLld6J8ziPrNysKx2DjF8ICAEhIASEgBCoxQRk9QorPLiNGjVCdna2FfZcuiwEhIAlEPD09ISrq6sldOWK7UNMehw+Xf0JTsQeQX5+Plwc3XB/zwfRt2kfi2Wy8OBi7A7fjdcGvGKxfZSOCQEhIASEgBAQApZHQJQOlndMSu1RkyZNMGPGjFLzSQYhIASEwKUING3aFHyJ1AyB7LxsPDPvKeTm5eCubmPg7+aLOTvnYPOpLRatdAiLP4W9Z3fWDDRpVQgIASEgBISAELBaArJ6hdUeOsvveL6yxrBxcLD8jkoPhYAQEAKXkcD8fb9j1pYZeLLvs+jT5Frdcl5+nnabq4M6yuHiPH7dMxfrjq1BTl4uegT3xN1d7oKdjR0OxRzGN5u/wZC2Q7BgzwJ4OHliRMcRaBcQquvJyMlQ+6fjQNReONu7YGjbW9CvWR+979M1ExCRHI7ODbrAx9UHC/f9gRZ+rXBv17vh5VwXP/7zMzae3AB3J3dc16yv6ts1qg5nRKRGKquMTxGRdBbZOZkI9m2m62Pbb17/eqnt6gzyTwgIASEgBISAELhiCUhggCv20FffwNOPHsWBB+7D7kF9sfvmgQifPg3Kfrj6GpSahYAQEAJWROBwzBHd224Nu5p6bWtjq9QNdfTnv46uxi/bZsFRTfh93X2xcM88rDz2t96XlJmMsNhj+GbD1/Bz98dx5Z7x4Yr3TPVM2vA/rD3yFxp6NVYKixxMXvM56MpBCfYJRlJGIlap/TO3focQnyY67/IjK/T+esriorlfS11u2vop+GrTNzrdwdYBrf3bwNPZS3/mNl/N/Zrrz/xXUrumTLIhBISAEBACQkAIXJEExL3CCg97WFgYpk2bhnHjxllc7/OzsnDsv4/BIaghgt/7BBkH9iPmxxmw9/eH381DLK6/0iEhcCUS4OoVGRkZGDly5JU4/Bofc7xSAtRVlgYuyhLhSOxRGJP+QM9ADG87DOtPrEMTNfn/eMhHuq9vLH1Lpa3FDS0GmPp+e+dRGBY6FKuPr8GkvyfgZEIYgr0bY0fYZgzvMgp3dRylLSZGfTcS606u0/Wy7iNK4bH95Ca8p+pupdoYl5uJ1MxUXS/r54vWEgv2/4H5yuXj8d6PoJ6LDx7sfj/OKSuH9arv3DYXWmaU1K55XtkWAkJACAgBISAErjwConSwwmMeHh6OiIgIi+x54sYNyM/MQOOXX4OLij3h1aMn0vftRcLC37XSIeb3+chQS34GP/u87r/+fEJ9fqbgc2ZEOCK/m47MsJNwCKyPwPvu1/XkZ2bixNtv6DK+w0cgbc9upO36B57X9UPAsNsQ9unHcGzYCIGj7jRxCfv4o4vSTDtlQwhcwQRSUlJwVFkkidQMAW9Xb+WqcEY3TsuFA5H7EJ8Wi3rKcoGKgQMRu/W+e2bdrd8zlRLApo5toc6G+rXWn71dvPV7Rs45nIwP03Eift81F0v2LdLpjBuxN2Kvrte8gub1mumP5kEhv9o0FVvCNiIlI8mUNS49HvU9Ak2fL7VRnnYvVV7ShIAQEAJCQAgIgdpNQJQOVnh8GzRogNzcXIvseVb4Wdg4uWhFgdFB106dETd7lv6YoRQM6f9sM3ZpBUT6rh2mz8dffh755zLgcU0fpG3djJNv/B9CZ/0C1KkD5zahup48pYDIS0qAc/OWiJwyAR6qflsPD8TMnA7/W4ep9p2QHRONxKV/oMGLr5nqlg0hIAQKCHio74uLi4vgqCECjZVFAq0NDkYfAl0s+Ppi/WQcjj6oe+SkLCBs6tjg4asfNfXQ0dbJtM0NxncoKozFQGkR0BqDWg8y7fZ19TVtc8PNyQN05zCX1cf/xooDf+Leng+gU2BHbDq9GXO2/2ieBY52jlqpQbcNe1t7076ytmsqIBtCQAgIASEgBITAFUVAYjpY4eH2V64KU6dOtcie5yYlwtbdo1Df7NUEh9YPyMsrlF70Q6ZSWGSHn0aTDz5F46eeQdMPP0F25FmcOxUGG0dHBN1zH+y8vJF55CDaTPsejZ99AS6hHZClFAy+Nw/WbSSsW6urTdq2Vb979exVtBn5LASueAJcdvf666+/4jnUFIChbQbroJHvLHsLSw4v18EhY9OiTd3ppQI4pmWmYNvpHbCtYwc3B3f4utYz7S9ug3l8lVXCIWU5QesDVwc3pRxwQICyoGCgyn1RB3RMB66ewe24jHhTVblqP4V1ZOVlYYNy8aD8E75Lu1twu5cKaEmhgmSnssbYfHqrduEoqV1dQP4JASEgBISAEBACVzSBix+VXNE4rGPwTupJvqWKrasb8lJTCnUvNy0ddt7qhtm28JM1nen8eVPelB3b9XbYW6+a0riRdvAAnBsHm9JcQtvrumxUfS2/mPJvevsuiF/0O+oNvB4pmzfBpW1H2Hl4mvbLhhAQAgUE2rdX3yGRGiPgppQBHw79BB+v+hgM2GhI2wad9Ob9ahlNukswICRflL6tbsATvR/VFhA64cI/I/ikkfb2oLcxftV4zNv5i34x/fkBL6OtWt3izcWvGNn0thH7gYl9m16HP5Wlw6d/faTzDGl/G8ITTmPGxq8R5Fkfnep3QKiyoGjfsAs2qqCWfFGm3jkdPsrFo7h2ezbuofPJPyEgBISAEBACQuDKJSBLZl65x75aRh67bAnOjh+H1j/8AqegBrqNY//3on5v9sF4nJr4GVI3bUDb2XN12qHHH9ZKitAffkb86lU4Pe51BIx9Ai5Nm5r65xwSAod6BebB++++A25XdUfjp5817Tc24letxOn33kDLb3/E4QdGI/DxZxFw23Bjt7wLASEgBCyOQG5+LpKVVUNdtfxkUZcHBmiMz0jQrhTcXx5hvQnnklSwSidlKeFW5qIMIsnVKui+Yb5tXsE5pRBJzU5T9brqYJjm+yrarnkdsi0EhIAQEAJCQAjULgLiXmGFx/PMmTP4888/LbLnXr2u1v069dH7SNm9G1G/zEbq1g3w6F2Q7ta2PXJio8AAktEL5uHcoX06hgODS3p07KTjQSQuW4zshHgVp0HdZNvYaIVDrgp8l3boEPKzMpETF1ewrWI7mIvX1dfo8qc/HKeTva4uaNM8j2wLASEALFu2TL+ERc0T4OSelgJFFQ7sGa0YuHJEeRUOLMt6/ZSrRHkUDizHFTWMeBHm29xniLNaytNPxYng/qJS0XaL1iOfhYAQEAJCQAgIgdpDQJQOVngs66igij/99JNF9tzO3R1Nxk9EjoqzcPzZxxA59Uv43/8o/AYP1f31VKtZuHfvjfAvPkHcvF/hNXgYchPicHbyF7D38kKTDz/V+WgtcfSx+3HixadVrAa1pNvePTj6+AM6b+rGNXr73NmzhRjYODjAe/CtOHf0IJxbhsLRz7/QfvkgBIRAAYHs7Gzs3btXcAgBISAEhIAQEAJCQAgIgWonIDEdqh1x1TeQlZWlYjKWHJSx6lste42eXbrC8+ffsO/O2+E54AbUH323qbCdqyuavT8euekqzoPazs/JQaPHn4KNXcGp6N6uPdrMmKUVDbnpabBTQSmpTPBSlhJeKzeY6iluw6vfAMT99hN8b7+juCySLgSEgCJw3iyeigARAkJACAgBISAEhIAQEALVRUCUDtVFthrrdVQrOdx6663V2EIVVK2sMahwSJj/q1rtsg68+/YvtIwmFQ4UG/t/l10zb5XLXjqUI2Bm3F8rkLZ7F5JXLdcBJL2vvc68OtkWAkLAjIC9+t61a9fOLEU2LZkAYz5w9QlvF68a62ZBHId07bJRY52QhoWAEBACQkAICAGrJCCBJK3ysFlHpxmHgcqA5L9Xwm/EKHhdc221dfzAfwqsKdx79EbQff9RsR0sd4WPaoMgFQsBIVCrCByIPoiP/vpAL595e5c7cWfHqrXg2q/q3x+9HyPb314qtx1nd+L9ZW/DztYeD6pVNAY271dqGckgBISAEBACQkAICAESEKWDnAdCQAgIASEgBCyMwMmEMDw//7/wdvfFI70e08tVOtk5YdHBJfh+8zcY0n447u0yWvd6xIzb8Ni1T+tlL8szjBnbfsCiPfMw94EFpRajpcWh2CP4acePOBS5D0/0+a9qr0+p5SSDEBACQkAICAEhIAQkkKQVngPpKh4CXyJCQAgIgYoQOHHiBE6ePFmRolLmMhGYuWOWWrzHBhOHTUSXBp1AhQMlJz8H+fn5WHZgMfIvxOXg51ylFKhO4eoaof6t8e6gd1HPwx8zlcKCS3qKCAEhIASEgBAQAkKgNAKidCiNkAXuT0pKwoMPPmiBPStfl7gEZtS8ufoVu2xJ+QqXMXduairCv5+BzMiIMpaQbEKg9hPgyhXz5s2rtoFGpUbj591zwFgEtUFqYjx7z/6Dq5v1u+SylGSamZ2Bjac2XYQ3LTsNn675HGPnjMXzC1/EtrM7THlorfD15ml63+tL30BCRoJpHzcycjIwcd0kPPzrw/jvgmew6tjfhfbzg42K0XNr+9uQnJ6AyJSoi/ZLghAQAkJACAgBISAEihIQpUNRIlbwOTo6Gq4XAjFezu6GTfgEu28eiDNTv6qSZjOOH0XSyuWInTkd0TOmVUmdRStJVcElY36YhoRVq4ruks9C4IolEB4ejtjY2GLHfzYlHDTZ33J6W6E8nIQyPSEjsVB60Q9hiafw2/afEJ8eX3SXVX6+3ONJzUrV1gxN6zW9JC9aQFwV0hsL9l7sFjFp/RRsVMfJz80f4Ymn8eGyd8H6KIsO/Ynl+xfBx7We/rz5xFr9bvybtOF/WHvkLzT0aoycvBxMVsqLmPQ4Y7fpval3E70drZRLIkJACAgBISAEhIAQKI2AKB1KI2SB+wMCAuDt7X15e6bMd1PXroatWsIyRb1XhfjdPAStJn8Nj74DqqK6S9ZRt0dPBL/3CfyH3XbJ/ZIoBK5EAg0aNICvr2+xQ/d29tKT3pj0mEJ5otVnmvLXda5bKF0+VC2BxIwkXaGHk3uxFd/W7lacjDkCWmEYQneHnUpRdLUK8vjeTePw6bAJetfm01v1+9ZTW1HfqyE+vPkDvHvjO2jgHWIUVSXPY0fYZgzvMgqvDXgFk4Z/qYNGrju5zpTH2PB09tSbCedKVj4Z+eVdCAgBISAEhIAQuLIJyJKZVnj869evj/Hjx1/WnqcdPYrclCQ0euVtnH7/TWRGhMOpfpDuw5mvpsAxMBCZp08h49BB1O3TH35DhppWkDj5/rvIjo6CQ/0G8B54PTw7dlI2uiXru3KVC0nYR+/D97bb4XlVN91O0bTsmGhE/fIzzh07iryUZNi4uCLokcfh3q49MqMicXZiwQ03C9uoZUY9O3XW9fBfSWVNmWRDCNRSAkOHDi1xZC72LjqeQLwyoT8SexRfb/oaT13zFOLT4uHkoPYpE3tOUn/dMxfrjq1RT8Vz0SO4J+7uchfsbP79Wdl4ajMmrPkMtirttg7DcW3I1SW2a+xcfGgJVh9djcQLlhLBPk3w+sBX9W66AHyzeToORO2Fs+rn0La3oF+zPnofn+i/tPAl1Z4tWvi1xk2tb0RTVZayL+oAZmydrrfH9hiLuXvnITY1BsOUq8B1Ta7R6SuPrcYSFaiR7YYoK4M7Oo5E83rN9D7+23p2G75Y9wU8nDwxouMItAsINe2ryg1v14KlMZMzk4uttoVvcx1bYcG+3015whJOIVdZKLT2b6XT6rsH6uO4/cx2tdpEfxyLPoReTf9dTjhU9f903HGd92R8mC77+665WLJvkU5jXXsj9mJ422GmNriRfK6gX1ROiQgBISAEhIAQEAJCoDQCJc/8Sist+68YAinbt8LOux58+vZTygQXJG/dYhp7+t7diJw6BeeOHoFjUANEfjURCevWmvY7Ng6Gc/MWyDodhhMvPIXELZtN+4rbsKtbFznxcYj++UdTlqRtW5G6dYNqo0DZcez/XkT8gl/h2DgEnspawr1rN9hfsACxsXeAc8tWcAwO0WVy4gqbkpdU1tSgbAiBK5iAu7JmoHvEodjDCIs9hiNxx5RbRTzquhRYWf2llAK/bJsFR3tn+KoVFhaqVRBWFokBsFhNiIN9miJFTZ4nrvoEZXky/rcy+Z++4Wu1TGQqOjbogl5KIdA6oI3pSJTkApB3Pl/lbYv6dRtga9hGvLjgWVObbg6uaFC3oR7L5PWTkKDGlpmTiUlrJuiAjAsPLsaUNRMRnRyh6gjF/og9eOPP15Cdl21qe+GeBWo8TXAq/gQ+XPGeKb2qN9wc3LSygIqAkmRI21ux+vAKU5Ygz/p6+2xyuH4v6qbhpdwq4opYrxiF3S9YVbQIaI1Hr3lcv54b8BJGd77LyGJ6P55QEITU393flCYbQkAICAEhIASEgBAojsC/j6SKyyHpQkARSNmwDu491dNAZaHg1r0XUjZthP+t/7os2Dg4oOXEyWoR1jo4cPQwkjdvRD1l1UCpP/oe/Z6XkYGTH4xD/B8L4NWzl04r6Z/PLcMQ/tmHyFJWEo7+AUp5sBmOjUJMFhZ5CXFKsRAKP2UN4dK4sW7bqM/BxwdBY+5X1hkpiJszy0g2vZdU1pRJNoTAFUzASykXqGQ4k+QEF0c3hCed1RP1em4FbhnrT6xDE7+W+HjIR5rSG0vfwnqlMLihxQATtRGdR+E2NTFmcMP7Zt6Nzae24KZWN5r2X2rDCG54g7JSGNiiPzgBN8TcBeCujqO0tcWo70aCLgB8Gl9XWSA8efVjYMDEtOx0PDT7Afx5cCnu7nwngr0bY7SyxFivlCUuSgHx0eAPsTtyL75WcQxi02Mxf/dcONg74Ye7Z6KO+otVsQw4aXewdTCax8gud+KWNkPw94k1+HL1BHBZyxDvYNP+qtxoG9QJ646uxNju9yuLDudLVj2geV+9fKaxk31trpQGKw8tg69bPaV4KVAOX63iP1C6Nu6GZfsX6mU367n6YM3Rf2Pd+CqFhK9HoF4Os5WyEmkb2FYrXAKKKBZ4DBaqWBIeLnUR6BFgNC3vQkAICAEhIASEgBAoloBYOhSLxnJ3REZG4s4777xsHcxVrgvnDu9HbmKCcmeYjfPnziFt+ybkZ2aa+uDctoNp0m/n44vzSsFAYZ6TH32AvcNuxp4hA5G6cY1yhzhsKlfShk/f/np33FK1skVeHlI2roOnct0wJPCxUW9esgAAQABJREFUp5F16iQOPzAae28bjDPTpiI/J8fYXeJ7ZcqWWLHsFAJWQGDBggWYMOFf96NLddlXBSJMUgEjzyadQavAUJxNPqvM6hPhd2ESeiBiN8LijuKeWXfr18HIPTgSdbBQVW39C9wPqDigW8Y/akWG0oRuAA19QjBrywytqHjuj+e1awTLmbsAsN17Z92jXQLoAkA5GHMYLyj3ilHfj8D9P96r952IL3Af0Bku/KMlA6VDYDtMuX0K/N38kHouCa3VRJsKBwon4U3MYh4wrbVvgduCl3OBtUdGzjkmV4vc3WW0jp/x3O/P4UD0QbUkZq5uh64thnAZzV5N+xgf9fuYq8ZoF5cfNn2rFQgDWg8C3SwoQ9rcDFqwzNj4NT5d+SFamlmQcP/bg95GI2XJMW/nL3jnz9d1EMp9Ufu5Syt4TigLh3eWj9PWIHd1vcfESmeQf0JACAgBISAEhIAQKIaAWDoUA8aSkxl13tPT87J1MXnbNt1W1snj4MuQZLUyhFf3HvpjHbtLn0oxixepwJOrEPzuhzruQ+SM6UjbWVCfUQ9dIc4rn/CiYuviAq+bhyFhkXqq1rmLUmBkwOvqa03ZaElRr19/pB0+jMSN6xH38/dwCWkCn/7/Pmk1ZS6yUZmyRaqSj0LA6ghs3769xNUrOCCazu9XioUMZaVwe6c7sHjfQuXykKLS/fR4nRj3oY4NHr76UdP4HW2dTNvcMOI7cLULLvHofcE1o1CmIh/cHd3x+a0TEKesLHYoJcXsHT/io7/e1xYI5i4Ag9Rk2hBf1wLri/ErP1BWDG54c9A4FXfBA68uetnIUujdcEMwT+R4jqqYByWJMZ6S8lTVPsaieH3QO3rsry/6P4zoehdGdRipLS1obWHIM9c+Bb4MaaWsT2bePUsvV+qqFD3mfaZyZfqo6XofOeefzzOK6Xfu/3ToJ1rBkaCUMC7K8sOwNNkVvhvjlDULV874T6+HdYyIQoXlgxAQAkJACAgBISAEiiFw6ZliMZkl2TII+Pn5IS0t7bJ1JmXLJjg1bYnWUwuCsLHhfaOGg+mG0qG4zmSdPa0CPLrAvq6XCt4YoxUO+coKImX3bnh0UNYRSlxD2yLut58Qu2wJHANU4DNHdaPbquCJYj21wkXi4vkIn/wF7H0D4NL03yXk4tf8DQcftfSb8uO2db5gfnxB+ZGlgkzmJCQiL72AU9aZM0g7dEi5ZtSHnYcHSipb3FgkXQjUFgJdunTBqVOnShwOlQsZWQXfn071O+K7Td/oJ+/GBJ+xFv5SQRe3nd6BHo27axcAujeYS0RqpHZTmL1ztk7u16yv+e5Lbh9TgQ3p7uDu6AE+ya+jFBvsx/nz57X1QUkuAFxZg64T7sodZFfELl0uLi0GrNPF0UW/s9FTKuAirQfa+Lc29aF7SC+sPrQcby57Gze2ugFOyqXB3sYebYtYA5gKXIaNjvXb4+d7ZyNajaGOmYVDWZr2VEqX4sTYZ1Pn0rcAVFT4XVhW06ijmQqs+eltE9GobiMdSNRIl3chIASEgBAQAkJACJRG4NJ3HKWVkv01SoBLZs6aNevy9EHd6KdsWAuf20YWas+9Z2/lKrEeeOoZ1LG1LbSvjtnKFH4q7kPG/n04PPYeNdmvC+8hwxDz4wwcf/YxdFq5QZfzUKtZuHXpgbPjx+nP3rfcblI6uLVsqYJBNlNBKg/C964xpnZy09Nx+p1XTZ+54d7rOvhcU2AJEf3bHMTPLZjocB/b5KvB86/A69o+JZZlfhEhUJsJDBtWeDWCS43VX7lXUOxs7bXvPif0FD4Np9zfbQzoXrD2yF/6xbS+arL+RO9HtQUEP09YWbDKDl0rnujzDPgUvjT589BSrDELjsj2H7n2SVOddAEYv2q8dgGgGwDl+QEvo2fjHrhb9Wn6pql4dt5T8HT1RteQnth+cpNacWIiQlRAS8ZzoCzZ94d+zX1ggf7Mfw90+4+O4cD8+87u1OmN1ER7wi2fmto2MhsuGMbn6n43mFd3OyXVT8sIvkSEgBAQAkJACAgBIVBeAnXU06Pz5S0k+YVAeQnkKssMOzcVEE7FZmDcBQaeZFBKc8lNTdVxIXQ+sx1R8+YicvJnCP15Hhz8CiZC3J2fnQ1dRm3bK3eT4lw8zKoybVamrKkS2RACQkD7+sdnJGgzfnNLBwaPzFCrQ5ib6JcVV7Jy48jKy1JlneGqLBcuNclnjIOiLgBG/QwAyQlyjlryMV9ZQtmrAIvmsRCMfJd6Z37TeJzVdeVCjIdL5ZU0ISAEhIAQEAJCQAgIgdIJiNKhdEaSowYI0JIh4a/l2oUjdcsGbeXQ4IGxNdATaVIICAEhIASEgBAQAkJACAgBISAEKkqg8KPmitYi5S4rgaioKDz00EOXtc3L3RhjMUROnYLc5GQE/fdFiMLhch8Baa82E5g7dy6++OKL2jxEGZsQEAJCQAgIASEgBISAhRCQmA4WciDK0w2uXsEI4rVZHJUbRYfFK2rzEGVsQqDGCBw8eBAJCQk11r40LASEgBAQAkJACAgBIXDlEBClgxUea19fX6Qy/oGIEBACQqACBFqp1WEiIyMrUFKKCAEhIASEgBAQAkJACAiB8hGQmA7l4yW5hYAQEAK1ggBjCJd3GcZaMXAZhBAQAkJACAgBISAEhMBlJVC7bfQvK0ppTAgIASFgPQRE4WA9x0p6KgSEgBAQAkJACAgBayYgSgdrPnrSdyEgBISAEBACQkAICAEhIASEgBAQAhZMQJQOFnxwiusaV6+49957i9st6UJACAiBEgnMmTMHkyZNKjGP7BQCQkAICAEhIASEgBAQAlVBQJQOVUHxMtfB1SucnZ0vc6uXr7nkHdtx/O03Ll+D0pIQuMIIHD16FKdPn77CRi3DFQJCQAgIASEgBISAEKgJArJ6RU1Qr2SbXL0iKSmpkrVYTvGMY8cQv2IZUrZuQn5KCvJSkmDvH4jjr78C5xYtUfea6+ASHGw5HZaeCAErJ9CsWTPEx8dbxShyzucgKTcJSw4uRXRyDOLS4uBg54Axne9DiHcwbOqI7ry4Azlzx09Iy07DyfgTilsM3B094Ovuh8ZejdEruCea+jQprqikCwEhIASEgBAQAkKgygjI6hVVhvLyVZSfn68bs7GpBTfbaizhM75F7NzZSrHQFJ5XdUcdW1vkpqUhOz4O2VGRyImLhWOz5ggc8yBcmze/fKClJSFQSwmcO3dOj8ySLaZic2Ixb/98HI9RSsnkOGRlZ8LGxhaODk6ws7VD+rk0eLv6ICL+DG4IHYIHuo2ppUer/MNad3oD1p1Yh0OR+wG1SomvdwBaBrVAZnYWbPJtEJ54FjFJMWjsHYKHuo9FgLt/+RuREkJACAgBISAEhIAQKCMBUTqUEZRkq3oC4f+bhNStm5GXkQ6nxk0QNHLUJRvJTU9H7OqVyDi4H963DEfgXaMvmU8ShYAQqB0Eftz9E9Yc+xt553PR2D8YTes3RVC9Bnpw2TlZSD2XioSUBBw5exinIk+q9Dq4o+vdaO3bCm38W9UOCBUYxa6IPVhwcD7iMmJB5XRwYAjaNGoDN2e3i2o7r/av2r0KETHhGN51JG5qcuNFeSRBCAgBISAEhIAQEAJVQUCUDlVBUeooF4GU3bsR9d005EZHwUVZMAQMHVam8il79yD2r2Vwv7YvGj38aJnKSCYhIASsh0BkahS+XD8JydmJaNe0PVo1LJsC4XjkMWzeuxm9Q67FmK73WM+Aq6inqVmpmLtvPnaG/4P8Ornw8ayHHq17wklZhZQmu47vwt5ju/GAsnjo1bhnadllfy0hsEZZwpyMP4kxV0lQ6lpySGUYQkAICAGLJiAxHSz68Fy6c1y9YuXKlRg92vqe+Mf8Ph+xv/wEp/pBaPDUs7CxK/sp6NGuPc4rJEnbNiNi5veof899lwYkqUJACJRI4Oeff0ZycjIeeeSREvNdzp1UOLz/1/vw9vbCiF4jy9V008BmcHf2wNIti1XcAjcMb1c2RWa5GrHQzLn5ufhu+0ycTg1Do/qN0KFJe+V+Yl/m3nZs2lHlzceMrdNR370+gr0bl7lsbcpIjueVK4p9OdhZ6/jz1Tinbfwa2blZ1aZ0IM8F+//AraFDYWdT9t95a2Uq/RYCQkAICIGSCVhkUADeDO/duxfZ2dkl974a9kZERODPP/+shpqrrsp05W4wf/78qqvwMtVEC4fYX2ejXr8BCBo1ulwKB6OLnkrx4ODlg7Qd2xD/92oj2frf1U1g8s5/Cr2sf1AyguokkHn2TKHzJVvFPimrxMTEWFwgyc/XToRvPT/07divrMMolM+vrh+u6dAHSw4twpHYo4X21eYPq46twankk2jRqAW6NO9SLoWDwaVj084I9A3E5M2TlEtLnpF8Rb0//8cLeHr+01fEmNeeXIuMrDRc26J/tY13d8Re/Lz1B8Skl/26VG2dkYqFgBAQAkKgxglYnNJh//796NmzJwYPHoxrr70WGzduvKyQNmzYgEcfvdh0nxP9I0eOXNa+FNeYq6srbFWwRWuTqB9mwK1pM3h27FyprvvfNBh1cnKQsHgh8rOyKlWXxRRW/tUnnn+y0Es5ZddY99IOHULUvLn6FbtsSY31I1utsMB+ZCck1FgfzBtO3LQRMeq8q4xEzZ+LdLVkZWUl5o/fC50vCWvWlLnKevXqgavgWIp8u3UG8m3ycG37ayrVpZCAEDTwb4Tv/vm+UvVYS+HM3EwsOvgH6nnXQ/OgygXZ7RXaG8mZSZj5zyxrGX6V9TMiNRJnlKsBLQBqu+Tl52HGpm8R7NsMj/eqPkunlKxkjdLH2dvqkW4+vRXncgqC71r9YGQAQkAICIEaImBRSocsNYF85513wAn+448/jqCgINx5552YM2fOZcND80rKvHnzMHPmTHz33XcIDw/HBx98gIEDB2Lbtm2XrS8lNUQ+1Slbt27V5tdV1UZmZASyz4TBu++ASldp4+gI19ZtcD4rE6n79la6vqqsgJYylZEGz7+CDotXosOfq6BC9VemqkqVzTh+FEkrlyN25nREz5hW7rrylZXSMbXkKa03KiNZypUocvJnyI4t29OytMOHdbtc+aQ6JHLqFIR/9iGQV/GnwZGTPkPqPzsq3b0GYx8uOFcW/VXuurp164Y+ffqUu1x1FMhSJt6bwjagc6tOVVL9VS26IjY1GkfijlVJfZZcyaGYI8hUk6F2wW0r3U3Gf2inXDO2nt6CnLycStdnTRX8sa9Akejm5G5N3a5QXxcd+hNpmSl4sd+LFSpf1kJJ55LVT5gNHO0cy1rEYvPN3T0Xy4+W/zprsQOSjgkBISAEaoCAxTjaUdFw7733Yvv27Rg5ciRefPFF5Obm4uWXX8YLL7yAHj16oFGjRlWCiG4btKg4qp429u3bF25ubnjttdd02sGDB3UbzzzzjH7v2rUrWrZsiSeeeAIeHh7YsWMHrrrqqirpR0UrCQgIAF/VKW+++aZ2b/H390dwcDC6d++OW265pcJNJq1fDztPL9gr1lUhdTt3RbJyschQx9CzS9eqqLJK6njqqaeQoJ7Kt2jRQlvsDBigXEmU8qysYuPgCBun0oO/lbW+iubzu3kI+Dr1xQSkblxf/mrUpDx14xp49FCB6TpVzrKlPI3nJCXqdvOrKdBok3feV8u5pkOZGpWnW9WS18Ze+e3zVYGns80rufQslWu8RowZMwa9e/eu1PgWHlwMb3cfBHrXr1Q9RmFXtVJDXQ8vrDq+Ci3qNTOSL/s7f8fatWunf2Ouvvrqaml/65mt8K7rA465KqStUl4cOHkAiw8t0b74VVFnRerYuXMnOnWqGiVUae3TymHFgQKXSlcH19Ky6/20iDgRfwL7Yw6oc6wFWvu1LFM5WqZwEl5H/ZUkDAxap04duDlU/riat5mRk4GflMvDoHa3wN/Nr6QulLqPMRsoxcVrSFZKByd7l1LrKS4DGeyO3Iv4jHjc3GpQse0UV760dFp8JCrLHi+nurBVS/GWJE72jjgWe7ykLLJPCAgBISAESiFgMUqHt956Syscvv32W3CiRrFTQQbfffddHWOBgc9eeukl7eLA9CZNmhQ7tHhlkj1lyhRs3rwZoaGhoAIhMDAQe/bswZdffonly5ebylLZcNNNN+G3334D3RZ4M05lxC+//KKVC+ZuDFSEGMJ17hctWoRdu3aBa93fcMMNoIKCNwpUoHA/TZgZeyEyMhIPP/ywVblELFmyBCdPngTdTciLN4FkRAXMY489poK9lc9kkjEYbB0cDHyVfrdRdTk3bIyMA/sqXVdVVkBGjEfCQJ/kRksZnnvk9txzz2kFV3nbC/vsE7g0bQq/W4bpovpzs2bwG3or0o8dQ+S3U+EzeCjiVJBOO09P+N42Am6tW0O7SHw/XZcJfOAhxC6Yh5yYaNQbNhxePXvp9OQd2xE7dw5y4uLg3CYUDe5/EHYenqV2MTc1FcdffUlNvu3goqxOfK6/ES5KOUWJ+HEm0i5YOMT99gtS1q/T6b63jyxQEKkb9pg/FiBRxeQ4n50Fz97XIvAOtVzqhYl8/Mq/ELfoD9RRnz16FPRTV1DKv+NvvoZsZVFDOfXxh7B1KZhANHr+JTj4+KhYefmImj8PyWtVLBB1k+nVrz/8hpRNkcYxZVywqrGt66X5Gt2JW7Ecafv3qmPUDAlL/4Rr2/bwv30EHHwLburpShE54xvkJiUp9rcbxQreS2DBmCWJyrWlwRNPwSmoATKOH0fEtK/ho467cfwKV3b5PtE1g+f1e++9By8vL/Tv3x+jRo2Ci0v5Jxl7le93A/8GVdp5fy9/ZS5/ukrrLG9ldNNbuHAhXnnlFdSvX19byt16663w4blYRXIqMQz+fv5VVJtaeLSODfx9/NWE+mSV1VmRisaPH48k9X3p0KGD/t3nOVYdQuXB+JXj4aKCj7o6upsmn4xD4KE+2ygeP2yfhaC6DTCo5fVqKdJ4fKUCMO4+s10vSco+NfQJwee3TtDdWx+2Ebn5OejT5LqLurvp1GZ88teH+Fjl9XT2BF2KDkXtR2PvEDzc8yEEuPsjLTsNz//+AmJTInX5ANXuS/1eQiOvhhfVZySUtc0mqp8zd/yorA9scXfnO43ixb4fV0qV7Wd3YGSH2y9SkkQpS6JnVPyL4Z1Gom/TPpccS+K5JMW1bEoc804sObwM83b/hoTUf63bQryaoH1gqClbfEbCJds0MnD/wZhDcLB10EvoFlXeLDq4BDPUcaTQGuPWDiNwZ6dRSFJKiCkb/ofDUQfQxLc5nr3uGXg6eShFkRMS0uOM6uVdCAgBISAEKkDAIpQOnKTTheK///2vSeFANwcGdOTNLBUHXLGBwsk7J7xz5841Dff999+Hu7s7nnzyST3BHzZsGFJSUjBo0CCtGGDZH374QVsz7FbBDHv16qVvBKm44E0yFQVUSNCSgW4FfELFp9PmCgf2h+4WdPc4rEy4H3zwQd0W62Db33zzDYYOHYqPP/5Yb2/atAm33347nn/+ed3Phg0bYsiQIaY+V2aDSgw+TSeX6pSQkBDwdffdd+PAgQNYunSpnkxTEUGGtEBxKuNTeRulmLFVr6oUpwYNkFRJ8/2q7I9RF59u8kWJVW4BPFfXK0sPKreuu+46fQ43UH0vq6T/s03dGf37ZIyf69gVPJnJTU5C6tYNyDx5HG5X9UCaWtkjfe9utJ09F3auLnBQ51383NnIy8yErTpWeelpOD3uTXgtXIZsdVN/4sWn4dS0pVY4JK9Yqp+aBz9TcM6W2L/z+XBRk+u8tDSkqEl83C8z0W7uYtjVrQtHtTLJeeUqlb5zKxwC68O5dcF5audZV1cZv3YNwr/4BO7de6OO+i5HfTtF5QuET99+yAw/i9Pvv6n75NK2nVrpZFaJ3TDf6dqmLeh6k3n8MJybt4Dthfa0RYDKyEl85JQJqt8dtWtO+OfjYa8mbF7de5hXc8ltx4BAnFdxRFI3b8S5Y4cL5ck6cxqJC+chPagRXDt2QcKiBZpL8AtKKaPkxIv/1e+eAwch5qcf9LbxryQWXj16ImraVwh77x20+OwLhKnjZuPohLpXdTOKV/h91apV+rpHS6+KiL2ysKArHBWTvC5QSUmXtNZK2UWLNeP8L61uPi2NVk+aQ1u1Li1rufZ7unribMQZ7SZQU6sRkK3Bd9q0aZrTTz/9hM6dO5eLUUkD59PgZq7NSspS7n0NfBvhyIkj5S5XlQX4kIEKe55TtLCj5Vjbtm317y+t76pKvtv2vY7lMG7wh5i6+RvT0/TX/3wNHYM6I0qtqLLv7E7dXIf67fDb7nnYeWqrDtb5dP/n0S6grV4txejPhAsKjKJKh+i0GHy2aryOo+Bk74wnfnsMuXnZaB3YHvvCd6pVW97DF8O+wLQtM7TC4erm/RDoGYB1x9bimXlP4n+jvoGf66VjsJSlzWDvYNCiY/n+xRh79WNwUpPo0mTS+sk4HXccfZv1VW3XM2Xnd/atpW/pzxx/cWM5m3QaKcra4WvFNVm5cwS4B6BTUEfFrPj7Fp7P09b/T9fdrcnVGNVxJPzcfOGsmBliKDwuxY95Vhxdia/WfqmzcxUXN6U0+GjoeNRzKVD2JZxL1AoHD5e6uL71IMSlxWPB7l9xJuks9qpjkZmdAX/P+vq4f7ftBzx9zRP6vKDFCGX27jlYuGcB7ugyGkPb3KzT5J8QEAJCQAiUTsAilA6MmUDhjYUhtCDgE3VaEFARQP/jPGWyfeLECT1xM/IxDgSfLjPwJIXuGJyUv/rqq2jVqpUum6MmC5Q33nhDT5QZnPKrr77ST54NiwlP9YSY4nDhaTzbMhcqHV5//XU0VU+cD6kge2yDygnGeqDlxb59+/TnqVOnarcQWlnwxRsmBqCk20h5lA6nT5/WdbIfdOno2LGjfrKSr57W0rSZN2RUatRVkzyjr9xHKw+OhS4knBgwjS/2nxMEuqhw20jjO91YqO2n8DOFeRITE3Vdxmem8YaZ/eHN4Pfff6+3DXa6YDH/8tUTbeQV1F1MlnIn26snzuczz5VYjgoiTvzJKDo6Wj+VNR97qnpiT0sVI438eDypjGKakc531sFjz/PUYJimJt1GefKi4opWLjyPjDx852SMnKhI4w01rWYeeuihEvtenp1+94zR7hCJG9Yj7I2XcO5UGJwbByNg5CitdLBVbi3N3n0fqcqtKOLrySowYzySt21VrhwuaDn5a3BiHqMm6tFqkounny01ngStIRo+9IiObZCrlIYH77sTfOIfMGKkVh7kqwlzzI8z4NH7Gt0v87GkbFgH9x7XoNl7H+rkk+rcS1q/VpdLuRDroOWkr0Brlki/AER9M8m8eLHbAcpaInHLZhWLYin8br0NTg0KPx1M2bQBjsHN0HLiZF3HvlHDkaKUCGVROvj0H6DLnM5IR/Lqvy7Zh6bvj9dtnnZyhFbgoOA45KYkoelnU+ChntqmHeyPo088aCpfEgu62TR+/R0cfex+HHzwPuQmJqDlN98phVPZL9vHlXUEr3U8Tx2VQsZQsvKaxCfJf//9t+m7T8UiJ3a8FlARy2sIvw9UkPGzcY04e/asvo4wja9MpdCiiwWvL7zO0cqH3xVajfGp/tdfFzxRNA3abIOTEdbrYOdgllr5TRvVL1sbO2TlZZVrCcRjynJoxowZeix8uk7XO46R1wlea7nN62VGRoa+lvAzhSsukS0ZU/ibxuuIMUEma/I4c+YM/vnnHyxevFiX79KlC7744gtdpqL/SjMNL2+9DeoFYfNepVzLPQdnu38ne2Wph9fJzz//XGfldY+/QWTEY0xGtP7jNl+nTp1CM2Wxxf1U8PN3mtdSbvNF5rSe4TnI84oKm9mzZ+uHDvwd4+8pz01aQ1REGBxw8d4FGNH1LjSt1wQZysogOjVPKRqiVUDJfPylnoZT+ra6AasPLcPeyP24o+MIHFVP0CMSz2D65mkYqywUejb+V2nJcXHCGqEsFX7f9wfuUJYATsqd4v8WvaxdDd664U28tuR1ZOdkYnD729BcBXM8HntE/TQW/DbuOLUFzQNa45lrn9Jtj+owEmGJp7QLgE64xL+ytMli7y1/D/WVxcQNLQdeopaLkxzUhJ11OyprAU6+uzbsirYBbfDR6o+1YoSKmikbphQ7lhylVOE4ybGFUjTQeuD3Xb/iqb7P4bom11zcoEpxV9Ylo666G3N2/IStJ9ZrZcN9Xe8ppHT4YOUHxbZJdwwqHK5u3ldZj4zFtjM78MXqTzF5/RS8ef3rus2tp5USX8n/DXgVLZQ1A+U/3e7FR0opRIXDa4PeRqf6HTB963daWcL9tHTgeCaum4S1Rwqu/99v+gZXh/SCt3P1WOGwXREhIASEQG0iUPa712ocNc10KVQeMFgjJ2x8wk53h2XLlul9d9xxh37nP96YGDJhwgR9U0IFAyeEvInuoxQUNPul8KbXcIug+8OKFSvw+++/Y9KkSfopFK0i6LZh9IE/shRjuU5O7tmfm2++WfeHMR94I0qhawZvLCm8WecNFCe2hpKDcSg++eQTPTmnmW15hPVOnz5d35BxMst2eXPGF29g45Q5PJ9WGpYGRr+phCEf3hizDuan8J03b3wZ9Rj7eKPImzzeJBp5+c6bPkOhwIk02+WEg9u8meT4aKVi5NGFi/lnr0zN85SipkpFmYnmq8lNSUIGn376qeZhKGGM8bMcJwnG8SJD8qPigMffyGf+Tn5GsEimswy5cZuTL7LnhMu42WY6rVJYhtY3/MynwJWJT3JeHdui4taqjU6yU4oYSl6mUvKYCa0AKO7KOqblF1P0dsrWLcjPzMDB/9ytP+dnZYIT5MyoSDgpa4WSRLsMfPct0nft0HUw77kTx0sqYtqXsmGt3t5/d8F3mpNpKhgoqbt2aisH47NbFVrzpG3fAvdr/n2y76yYpG3botut7D8qbwwlB2OXkCMlde8e/e52QaHqptxszKUkFszH/D7D71SKo58R8NCTpR4X87q5zXOR5zPPcZ7rfPF8Nb7rRh5+Zl6e/8Z+luE1gNcfXiMo3MfvgHHeM53nPc93CpVrVHDQcoyKY7Zdkqie6O8EJ3lVKdm5Odo0nubx5RUqGzhm/nZQkWCw4e8RGXHsvAYaDLifHPgiM+Od6WTDd+bnO39nyJSfeQ2nsroy4qSUAumZKsZIFQqvUXY29kg+lwJn939/a8vSBB8KMNgy3W84PjKi8DeTn3l9ZP1kwd8TKqq4zd9Y/pYYv6dM47lonK+sx/iN5nKvbIf3Bvz9r4jSgU/9P11ZoPTkU+tft/9kGt4WFScjLTNVf75OTdCf6P2o8uc/gsMxh3FDiwH48rYvsVW5V3yvJqV0l6jn4Y9Hez+OjvXb6zKh6un/x6s/0VYC7uop+y719DxVuRp8NPRTRKZE4Wx8GBr4BGPRnnk6P5/GU3lB4TKWvm7+etv4F+zV2Ngs9r2kNjmR/0G5VcSkRGDSiK+g7iKKrcd8B90wglTb8/f9rp7sz8N+5QrSNrAd/gnbgvt7K3dRtb+ksdjUKbDGmzH6Bx2bghYSd34/EssPLy9W6cD2R7S/HTe2vAGzlOJhlXK1WHN4hVL8XI+x3R/AqcTTJbY5Z9cchPi1UEqbp5UiJw+//POzHtIepXw4qgLLNlcxXuLT43Wav3JnMYTuFzFK2dTMv5VWODD9/m5jjN36nA1POA2+qBTpEtQFLyx4BjvO/oOBzfub8smGEBACQkAIFE/AIpQOdE944IEHwHgOXCaTJqm8QeFNCIXWCHSp4I0HLR8Yr4E3L1Qy8IkRhe4Mxo3vxIkT9c0Kn/LRMoF5KbwBpMKBfrW0QKBJMH1HqVBYu3atvjHy8yvww+YkkS4WH330Ee677z5dnn65fFJFtw0KXSpYljdOVE7wKRnz0/qBN6jsJ2+mOMkcN26cfjrGm9iyCNuiEuZSwr7RpPn++++/1O4qS+MNNt0C+OSPE2YqMvr166fHz/fyCP3dk45WrcluboqKjq3MzUsSHn8em8stVDTwyRzPTyowGKfk+uuvR4UCyvHGPadA0ZCvzuGc2KiLggca7hbFjdNBnU9FxfbChLD+w48XCl7p4POvKa2NvbLYyLtYyXHq/bdhq6wdmn76pYoB4Y7jLyrrCHNRSixKblKyearetvPy1pYUDZ58xrSvjkPBE2Idu4DuJBUUW6eC73pOcopSAhSuxLFpC2SfPWNK5LZT04InXabEKt6gqwklKyZaW54Urb4kFszL2BnJq1foYgkL58P/llsLHaui9RX9TKscWmgVFVrbUOguVhVCRQOVpAzQy+sgg+3y2shrbUlS18lTT9Izss6pYJIl5SzfvnRlkeJgqyw7yhnIjopjwyWufC0Wn5u/ZbwGkc2WLVv0bwN/x7g0NK8JlZEAjwDEJMWgdaPWlammUNmc3GzYKmUNlQPlFfLjuUXli1HeXDFFZQJf/G0xlA/cTyWPobzmZzKj0od5KbRWo0KCrmr8raaynddS3i+UV6hweOn3F5RSyhaN/ZqhlX9r7fc/Z+ccpGQmY0jrwfhBLSlJZcITvR/T1TPPThXfgPL7gYXoHdwLk4dPwq6IPfhm01S8u+QNvHnTOOV24YCl+xcq14kC68r5O3/RZZ4f8DIYU4ETeMrbN76tLQhOJIShiXdwoSf5GdnlUyKV1uaJhJPawoBuFeUJHkkXjBNqdRQuJUo5oZQufA1QLgkM7FjaWDo26KzLfrN5Ojo16IhNYZv0dz1ExbAoSdjf00ln8Eivh3BP19H4ZddviukfCFOseof01kWL48cYDmHKJYQuEHRNiU6OwOjuY/DrP7PxztK38I46RplqtRxKeo56YKKUQoY0922JTcfXYpKK6XBb+1tR3z3Q2KXPC36gEopKEQrjgOw4I0oHDUP+CQEhIATKQMAilA7sJ4Nt8UkGFQ+crPFJ8F133YXg4GAwZgPTx44dq59a8+kZn17TcoETYgrzjB49Wm9z4k9LB1ofUIybGN7k0OqB+2+77TZ9k0OLijVqfXsG/+NNIPvAmx26dhhCE08KXRyoDOBKDo888ogOSsnAlBTeYPOGiE/66PZACwrWQ+GTGI6HCgsGFKyssA/VrXCgDzInzLz5o+k02dO1wlDglHcMnspvPkoF0+NSisZT7PLWUTQ/n6w7NmxUNLlGP3NSwfgkfNrHc4sKNMPSpqIdc+3QGanr/0biup5I2Vlw45uj4pRkx8WWWCUnugw+SMk8fQpcTtL8SbvXtX2QuGg+4lXwQ58bb9LxDdRdvo6LYFTsGtoWcb/9hFgV0JBxDajkcVNWRefVpMHWzR12bq76aX525FlVPgAZSgHnosbNY+zWtSc4UXZU56uDip1g7+mhrQHq9huoXS9SdmyHR7fuuk77up66SQ8Vr4BuGeHfTYdH126InjPb6EqZ3t3atNEuI1EqgGb+iDtAZYZLSLAOjunR+2pwuUoGhaS1CGM/+Ko8pYoaa5oKBklhwE2lfdNBOumSwgCfJYm7iktBCZ82Ff533IWUrZsLZS+JBTOe+uQj3V7zKdNx/NkncPrLiTBiRRSqqJwfqkLZQHcMBtylmTwVvFTYctK5YMECvV2WLjHyfVDdhjgVfQoN6hXREpWlgmLyxCbGoKVP62L2Xp5kXgvoOsEJM5XQ/L2i60FVrsrQsX5HzN33a5UOKFXFfVG2cSYf+IpUbjwAMMpS+V6S0HXFXOiGYQgfDvBhAa1n2qjvN93S+PtdEWHgSCocKBOV0oDBGw1ZvH+xniRT18LJ5UBl1UA3HUqfptdhtXriTlmglk+kUqJdw846TkGgcqeIUvEANpzcgFbKjYBP1a8PvVkFf2yE2dt/xF1X3WtywQjyDNJ1fKnM9F8d8ApClTKDwn7FZcTBwd5Ju3boxDL+K6lNWhB9pCw6QlUshRtVIMzySKcGnfRYgpULyL1X3adiUnyMHmrST2UApeSxxOtglRHJ4Vh/dJV+MYbCDaFDMEbxKEnCEk5h8prP8f2W6ejcsJuyurHVriknlQLklra36KKX5hePESro5dtKAUTLFQbhfH3QO9oCpXP9Tnjpj+fx/Pz/4g5lqUApal31uLJoyVFBQOlKwxctUBgkdKA6FxgXhv1/rNfDuiz/3dZxBJYe/NP0WTaEgBAQAkKgZAJ11JOEgkcJJeer0b2c2NNNgaaUpd1sMG4DYw1QecA4D4y9QCsImgQzACJ9celjzDgRtKTgjTKtHjipNkw7jaf7XMueCgRDecAn/cRl3FDxaQ2f8NF0lO4JJQkVH4apaUn5LGXfPffco31mqUzhDXNVyOHHHoJrSFPUUwEDq0KOfzYe9R9/Gl5qMmkpQpNfnmdUOpVrcqHOpZ3XX4tGr7wNI36AMSauUHF6/Pt6kuzRZyBy1AoN5w7vh/9/HgaVAieefxKtpv+on6QzZsOxpx4CJ6kJK5freA5GPXzvtHKD+UfELF6o4zgY7gDOLUPRaspUU55cZeFyctzbSNtRMFn2vuV2NH7qGcSv+RsRkyYgNyFOx0lwCW2HxMXz4dqhqwp6OFGXT9m9C2c+Vv63SiFB8R46HI1VvAgqns5O+7pQ3+rdeR8aPqhuZhWH4ypwYsqav3SZuv0HqRgNS/R4zBUmemcx/6Lmz9VjousIpdEb78Hnuj7K5aHwWBhXIuTVN9QqFyVbH7Hc3mGDLmrNzqMu2s1fjPAZ3yrFzGx0WFwwKYn86UcdHNNgHfXbr4j8X4Gfu3u33jh3aD98R90DxqAoiQUVPWfHj0Pw+5/quBOMmXHmw7cR/NYH8LrG7AmvuibtHHA1Ah97BgHDC57CXdTZKk7gJJCKX1pu0ZqBlg2MT1ARWX18Debsno07+hcodytSh3mZ+JQ4rPtnHR7oPrbEoHXmZap6m3yoYCATKog5Wa4O4STp8bmPoXfHa9RT+8ZV0sT6XevhZuOB/17zZJXUV5FKaEVI1xPGD6K7CwPwDh8+XCtyK1KfUeZczjnlajAL96gggEWtYPYq94E/1BP1V/q/rFQuBcoGoxzfqRigEiIhI1GvArHr7HblhqEsqpQ1TaiKAfBgj/vhqrajU2MQ7F38sfhg1UfYfnKTVjB0bdwdsSrI5MnYY9o64sXrXwXdKcpjkcAxFdcmx/TJyo8w6fbJOl6C+XhK26br01HVr2bKJcFQvhQtU9JYJo6YjAYeQWD/UlW8jOICYRatk5/XnlyPuWoFi6ikcL3bRwWTHNZhuHZlKK3NQDel/FZLg9KtxFwYCDJFBapkQMntyi2im4pRcSmhCwdjQxyMPojTCWG4utm1uENZN2QpCwnzgJZ03zgSexStlUuGiBAQAkJACJROwCqUDpzo01WBq0dQgWCYbRY3PCoNGO+AT5oaN26sLRQ4eaa7RG0QPjkjA/OnQdYwLk4GE+bOQbBSFBgrClS037ErliFdPb1v9b9vKlqFZZW7oHQw71SnFeu0G4KRxoCNdkoBlK98nZUGq9IMjXr5zol1vlKqOfCJI905igjN/NVJpywb3ArtYbqdco/KV+bU2kpCmUAXKq++u9nK1aSOqtOewVrN676wz8be7qJlOvOVz7vS5Okxsu7yWsfQkiEnKVGVc1R1/2tCy87nKcUj+8NAjZdL2J88ddyM46fHo3iapAQWpjxFNs58NQVxv/5oSi2P0sEwb6+oIpQKWCpaS7sWmzpXysajcx9Fk0ZNcVWLq0rJWfrujXs2qpgm+Xh94KulZ64FOaZtVW4tsXtxy9Ulu7KUZahxybFYvGEx3lQBD7niQU2J4aLYp08ftG9fECuhpvpSHe1y+cy1KlDi4egDKoaDn3LxaKMDPNb3+Nekv6ra5RP9isQ2KWv7l3MsRp9qok2jbXkXAkJACAiBihGwCqVDxYZWe0sxMjqXq6QbilWJsvY49OhYOAc1gP9Ngyvc9Qy1skfE7JkIeuZFePXsVeF6LK0gV54wF0uy4DDvl2xbBgG6smRFFDwJZI+cQ0LKHGiS8ReoNDDi1dT0iNYp0/SZO75Hn859UN+n4srh6PhorP7nbzx73bMqMn2zmh7WZWk/UQUpfGXJKwgJCkG3Vt0q1ebv6xYg1LcdHux+f6XqkcJCQAgIASEgBISAEDAnoB5LilgbAbqMGK4gVtV39XQ58MGHEf7lBKTs3wcP5RpQXslUsQyi5v8Kz/431CqFAzmIkqG8Z8OVnZ+xM/iqiDCGTWkuYRWpt6JlrlG+4n8dXY5NaqnG/l0Hoq6bsrgpp2TlZGH9nvW4Ri3Hd6UoHIjIy7ku7ul8L37ZMxsHnPejTePQcpJT2ZWlzZKtS+BhX1cUDuWnJyWEgBAQAkJACAiBUgjYvqWklDyy28IIMD4ElxvjkpXWJk7KxSU7XS2/uXKZDiDopIITllWSVBDF6D/mw617TzR6vOb8jcvaX8knBCyVAJe+ZUDaiiw3WF1juq5JH+VHfQQbDq6Dv48/3JwLu/KU1G7auTQs3bwUzX1a6KX1SspbG/c1VEHz8lV0pk1H1yMxIwkB3gF6ScOyjDUlIxlLty+Dfb4DXuj7ApzsHMtSTPIIASEgBISAEBACQqDMBMS9osyoJGNVEoic/TMSlyxU5uANUK9PPzhcWOnjUm2kHjqIpE0bkBUXg3oj7kLA7SMulU3ShIAQqAUEvt36HTaGrUWzhi3Qs03PUke09+Re7D26Bx2DOqvo8o+Umr82Z1h/eiN+2TkbuedzEBrSFk0D1ZLRjs6XHHJSehKOhh/FsTNHEeTRCE/2flxbTVwysyQKASEgBISAEBACQqASBETpUAl4UrRyBM6dCkOEWhoxY/dOOPoHwMHXD/b1fFXwQDsVBDAJ2dFRyIqK1HHEXTtfhaCHHoFdkaXVKtcDKS0EhIAlEtgVsVutaDEH8emxCPSrj5DAJvD1qAd7OwflCZCP+NR4nIgMQ3jMGdTJr4Nb2g3D9c0HWOJQLnufGKH/z8NLsPbYGuScz4anmye83X2U8sFJrUdwHmnn0pGUkois7Cw42Drg+haDMKicyyle9kFJg0JACAgBISAEhIBVExClgxUePi7/STGW8rTCIRTqco5aOSFBLQeYcegAspWSAVytwNUNDoH14RYaCq/r+sKmlGUNC1UoH4SAEKgVBLaf3YFVx1bjVOJJZOdmqzGdB5eqc7RzQlDdhujeqDv6Nr0OdjYSnqjoAecSf7uj9mCbWtoxIiUCWTmZsK1jB08nT70yRah/KEL9Wwu7ouDksxAQAkJACAgBIVDlBETpUOVIq7/C02r1hldffVUvIVr9rUkLQkAI1DYCU6dOhatafnX06NFWMzSu0pCVlwUXexd4OLpbTb+lo0JACAgBISAEhIAQuNIJyOMhKzwDuGSmVa5eYYWspctCoDYSOHz4MFyszHqIqzSICAEhIASEgBAQAkJACFgfAVE6WN8x01HnQyq4VJ4VDle6LASEQBUTaN68OTw9Pau4VqlOCAgBISAEhIAQEAJCQAhcTEDcKy5mIilCQAgIASEgBISAEBACQkAICAEhIASEQBUQsKmCOqQKISAEhIAQEAJCQAgIASEgBISAEBACQkAIXERAlA4XIZEEISAEhIAQEAJCQAgIASEgBISAEBACQqAqCIjSoSooXuY6Tpw4gTFjxlzmVqU5ISAEaguBSZMmYc6cObVlODIOISAEhIAQEAJCQAgIAQsmIIEkLfjgFNc1Lpnp6OhY3G5JFwJCQAiUSCA6Ohp5eXkl5pGdQkAICAEhIASEgBAQAkKgKgiI0qEqKF7mOho2bIicnJzL3Ko0JwSEQG0h4Ofnh8DAwNoyHBmHEBACQkAICAEhIASEgAUTkNUrLPjgSNeEgBAQAtVBICwsTFcbHBxcHdVLnUJACAgBISAEhIAQEAJCwERAlA4mFLIhBISAEBACQkAICAEhIASEgBAQAkJACFQlAQkkWZU0pS4hIASEgBAQAkJACAgBISAEhIAQEAJCwERAlA4mFNazcfjwYXzyySfW0+Fy9HTFihUYN25cOUpIViEgBMpLYNmyZVi+fHl5i0l+ISAEhIAQEAJCQAgIASFQbgKidCg3spovwCUzd+3aVfMdqYYenDt3Drm5udVQs1QpBISAQWD//v3gd01ECAgBISAEhIAQEAJCQAhUNwFROlQ34Wqon8HfXF1dq6Hmmq+SS4EGBATUfEekB0KgFhPgcpmyZGYtPsAyNCEgBISAEBACQkAIWBABCSRpQQejrF2JiIjA+fPnERQUVNYikk8ICAEhYCIwf/582NraYujQoaY02RACQkAICAEhIASEgBAQAtVBQJQO1UFV6hQCQkAICAEhIASEgBAQAkJACAgBISAEIO4VchIIASEgBISAEBACQkAICAEhIASEgBAQAtVCQJQO1YK1eivdu3cvfv755+ptpIZq/+OPP2rtyhw1hFSaFQIXEThw4AAOHjx4UbokCAEhIASEgBAQAkJACAiBqiYgSoeqJnoZ6ktISMDq1asvQ0uXv4k9e/YgLCzs8jcsLQqBK4jAwoUL5Xt2BR1vGaoQEAJCQAgIASEgBGqSgF1NNi5tV4xA3bp14eTkVLHCFl4qNDQUbm5uFt5L6Z4QsG4CKSkpiI+Pt+5BSO+FgBAQAkJACAgBISAErIKAKB2s4jAV7iSjzo8dO7ZwYi35NGzYsFoyEhmGELBcAs2aNYOvr6/ldlB6JgSEgBAQAkJACAgBIVBrCMjqFbXmUMpAhIAQEAJCQAgIASEgBISAEBACQkAIWBYBielgWcdDeiMEhIAQEAJCQAgIASEgBISAEBACQqDWEBClgxUeyu3bt2PBggVW2PPSuzxnzhxMnjy59IySQwgIgQoTSE1NRVpaWoXLS0EhIASEgBAQAkJACAgBIVBWAqJ0KCspC8qXnZ2NtWvXWlCPqq4rXL3i2LH/b+8+wKMq1j6A/wlppIckpAHSS+i9RIpgFwREsKFexauIF0VF/Swo9nIRsKGCilK8KCqIiAii9A6hSIcQUkghhHTS+eadcJYNBFNIOVn+8zyb7J4yZ87v7AbOuzPvHKm4ClkTBShwkcC0adOwadOmi5ZzAQUoQAEKUIACFKAABSpagEGHihatgvrs7e0hySRtsYSEhKBVq1a2eGo8JwqYRiA6OhoHDhwwTXvYEApQgAIUoAAFKEAB2xXg7BU18NoWFBTghhtuqIEtL7nJI0eOhJwfCwUoUHkCgYGBkOlpWShAAQpQgAIUoAAFKFDZApy9orKFWT8FKEABClCAAhSgAAUoQAEKUOAKFeDwiiv0wpfqtFWPg6zoqFJtyo3ML5CdEI8ClQ+EhQIUoAAFKEABClCAAhSgQFUJMOhQVdIVeJzw8HDIo9JKfj4ip3+EsOv64OAjD5brMDmnTiHupx+Rk5RU7P5xC39ExuHDF6379ttvMX/+/IuWc8HlC0S8/SZ23XQNjkx8AXlq9gIWClCAAhSgAAUoQAEKUIAClS3AoENlC1dC/TExMfjqq68qoebCKo9Nfg+nfpyPgIfHoeXMr4scJzs+DrtuuU4/8lJTi6yzfpEdF4fYT6Yg5+RJ68WW57EfT0Haju2W18aTUypYIY8ruZz8/TdE/PfdCidoPHESGjw/CRk7tuLQ+P9AJc+o8GOwwpohMHHiRJudAadmXAG2kgIUoAAFKEABClw5Agw61MBrHRkZidOnT1dKy7NOxCB5+RIEjB6LwDvuhHNQcJHjpGzZrF8XZGUiZdvWIusq4oWrqyvc3NwqoqoaW0fW8eNI27CmwtvvWLcufK+9Do0mvYnsiCM4vXFDhR+DFdYMgZMqGLhv376a0Vi2kgIUoAAFKEABClCgRgtw9ooaePmCg4PRpEmTSml58saNul6/QYOLrT9143p4hPZFfnoaUjdthM+AgZbtTq38A4lLFqOWms7To2dvy3J5IkMpYmfNRF5yMnyH3V5knfWLjh07Fpm9ImVnGE7+uACO9epBbsa9rhmApKVL4H/3vfAOvVrvenrzJiT+9APy01Lh2qETgu9/AHbOzkhXUwLGfVPYIyRw9MM4uegn5Kq8Br7DhsO7l2rf2bNIWLwIp1f9hbM52fBU5yWBFjUfqXWTLnpekJWF8Fdf1sv9ho9A+u5dSN+5A579BiBg2G2AnZ3OhREz60vkREXCuVETBI1+CE7+AXqfiCmT4dK0KeoNGXb+dbNm8OzRE1HTpuDMkYPIS03Gkeef1etre3ii8fMv6ucFZ84g5ptZyNi9E7Xd3OE7eAi8+/TV6+RH4orlSP7rD+QmJuplzk2bo/Fzz1vWyxPPrt3g4BeA5HVrLYZFNuALmxeoqwJQ7dq1s/nz5AlSgAIUoAAFKEABClS/AHs6VP81KHML+vfvj6effrrM+5Vmhxw1fMK+ri/sPTwu2lySEKZtXg/3bt3h1q0HUtevtnTRz4qJRuRbr6AgIx1ODRvi5Hdzi+wf/ux4nNm/Fy5t2yPh29lF1lm/6Ny5M7p27WpZlKu+kU3bsFrFB84iI2yLDnTknojWgQjZKOdUIiJeeBrZx4/B3scXid/PRczsr/X+9q4ucGzQAGlb1iP6kw+Rp7bNV+2LfOMV3e5Ta1Yj5sPJqF3HGQ4BgYj7cjpkWYmlVi3UCWmjbvzDEDdvDlLWroKj2j92+lRkRkTo3Y/+3wSkb1qvAg6NkbzyNxx75SVLtTK8IfNYeJHXZyKOoZa9g67XoV5hcEKOoR8tWlq2jZr5GRIXzINz46YoyM5CxKTntYFscHr9OkS98yryUlLg1rW7CoJcA9eQEMu+lieq/c7NWyInLtayiE+uLIG33noLoaGhV9ZJ82wpQAEKUIACFKAABapFgD0dqoXdvAfNTz4Ne2+fYhuYtme3Xu7RqbO6ec9ArBpikX7wINxat0bqufwMLT/+DHaOjohVN85xMz/W2585HqG/uW86ZTo8OnRA+v6BOPyfh4o9xqUWBv/rQaSqb/C9B1yL2i6uqjfAIb1psuptIaXVjFk6UHLs3beRuk4FDh4eA+cGDREw8k6dn6K2GrLR7PW3kLZ3L058/olKcHlKBU3Wwr1nHzR78x1dxzHVQyF53Rr4qN4U/1TsnJwQfO/9SP59KbIO7UeHxctQkJurb+Jlhgg7RwfkxEaj0Wvv6p4EiSpIE/XOayo4cAqOPsXbyvFkndQbpXozSGBFnhcpKvAiBoFjn0TAcNVbRL3ec9sgJKthEvUG3Ypcde2k1L15EOr2HwB7NVTlUsXey1sHai61nsspUJJAjnrPOzo4lLRZmdeH7d6HNJXotG9ojzLvW9YdwiMicUw9pDir3lGhPc8HPMtaF7enAAUoQAEKUIACFChegEGH4l2u2KX23nWRF7at2PNP2Vx4g5+0ciXOntsiZcsmHXRIU8MgnJu21AEHWeXWpo2lDiNY4daihV7m1vL8N/eWjc492bx5sxqdYIdu3bpduKrwtVpnXSQZpWNgfUvPDPlmX3JSSA8IR9XzwSiuIW31U3fVrpYfTtfPU9ev0b/3jrpD/847nWRpv15Qih8ubdrr4Rh2akiGUW+CGmIixfXcebqe66mQtmtnkeEoRvVn8/KMp//4+0zkcR28OTl3Fk4tXKC3lWEYGbtVMEgFHeqqng0pq1chZso7+uHePRQB9z8It1atLqo3T4JLPn4XLecCCpQksGHzdqzfvBX5+QWwt6+N/lf3RtdOpRuqcex4FP5YtRa3D7kF3l6exR7qr7XrkZOTqwIA3dRIp6Kf92J3uIyF0TGx2HfwMM5kZau/O7UYdLgMS+5KAQpQgAIUoAAFLiXAoMOlZEy8/Pfff8e2bdvw4ouF4/wrsqkO9fyRl5Sop1S0d3cvUnXahnUqV4ILTv2yUC+X52kqxwNUDgXn4PrIVMMGiitO55JRSi+AOlc1Km4Ty7IdO3bA29v70kEHy5aFT+o0aYrUVSsgQz+kh7CIWT4AAC+ySURBVEV2dLRuo6MKnlgXx6Ag65f6uQRYJP9C/XFPWtbVcnSyPC/NE0eVX+PCUkflcJCSHRsLR18/ZJ04oV/XaVy4XI6J3MJAg+SHyD0Zp3st6I3UD+lJIcEE6T1hZ/VNsuRwkOLSuRt8b7xZP5cfDueCK/aqN0fz997XPSokwBE/ZxaOq6EkbeaoKUjVkApLUT0kso8dhUu7jpZFfHJlCTz//PMYOnQoevQoW28C6RmwZsNmBPj7oUuHdti0bYcOIgQF+iMooF6JiGlp6TiVlIys7JxLbnvn8CE4cyar0gMO0gDpTSGP+T8uRvSJ2Eu2iSsoQAEKUIACFKAABcovwKBD+e2qbU+5MT+ukipWRvHq2VPlJlAJCZf+igBJqniuSM4GPWTgrffhrRIeSpHEkZLHIVfNpOGhhhAkzJuFmK+/gofKJxD/vbrRPVfc2xZ+CxrzxQz433E3UlXviEsV6VZtb1/6t6WXSlgZ/9VnOD71fbiqXgzJy5fCo+8AHUyQIEfm0aP6UFmql4AeCmLVy8JrwHW6zanbt8Gjew91s69yO1zi21fr9spUoRJIkJwKkrBREla6NGqkk1fKdu6qt4UEZGI++wS+Q4fj5IL5Ok+GS+PGuhrXDp2Rtm4VTq/thdSw7XpZrppiNCfxpA5SePbohYTZXyBa5W/w7tNPB1Q8O3fRwy/qtGqrc0W4tm4D19YhOJunurj7FfZYyDx2DLmqh4cEJ+zq1FEGtdU1O1YY0LAKOqSo3iFyLYMeeUwfmz+uPAGZveKgGhpV1qDDpq07VPyqFu67c7jukdSieRNM/eQLyPLbBt+IH39eirreXkg8lYQTcQnw9amLYYNugItLHXwxez7S1bAsKT8tXmr5nI8cNlj1evDAr7//abnxd1F5Vpo2bljkwuz+ez82bFHvXdULIjgoAINuHAgnFWiUHhdfzP4fOrYLwd79h5CtApBdOrZD9y6FQbWMzDOYNfc73e4A/3ro0bUT6qv9WShAAQpQgAIUoAAFqkZAfeXKUtMEZIaHypq9QnoseA28EbEzPkLCzwtV7oMkzZO6rbAXg0c7NZzgXHHv2Ek/S1E37e4q6aFHv2uRMOdLHHniEZWc0cXYTPdACHx0vE4IKevOHD6khkN4WdZbP/FTN9BeXlbrLDfLVt/Uqx1khgwpMguEz/A79ZCKmKnvorbKVRBw5116XfwP3yPipWf084SvZ+Dw2Af1c+NH4Kj79L6SmDH8mcdx5HE1w8VvS43Vl/wtw0UOPzZa9wiRJJfy/IzqYWEp0nviqefUtJThOrFjTkwU6o8vbIds46dmrbD389dJICXpY52WbXSyy8Rlv+kqZPiJ17U361wUR8Y/ivBnn0DOuSlSm0x6Ha7tOyH202m6vUefegwZB/br/U4tX4bw58br9sh556seKw0nvqEDMLJBnrrhk2STES8/r5J9NubMFVrtyvzh6emJ1ioXS1lLckoqfOp66YCD7Cs3/e5urkg69/48ERePLTt24aQKOsh2UTEnsPi3FfowDYID4eVZmKBWghHBgQH64eBQGGT0qeuNQBUUyFJDHeISThZp2unkFCxd8ZcOWri4OOPw0WNYsmyl3qbgbAFk/er1m/Wwr4KCAvy5ZoMOfMgG8jowwF8d2xMRanjH3O9+QqrqccFCAQpQgAIUoAAFKFA1ArXUrADG8PyqOSKPYnoB6dYf+cFUnP7tZ/2NfYdfC28aStNwmdJRppyUYQF6eIC6KTGK5C7Iz87WCQ4L1G8ZDlGk27+xYTl+67ozMy25HcpUhfoISHDFTt382KvpKSusqHolqGCvbnaKO08JAkiyR7GQIRfWQymkDTL0Ii89TSfOrO1yPoij25efjxyVl8HOuU6RhJHSC6NATf9pp4I+9rKPJWgDHHzycWTu3g7XTt3R5OVJFXuuFYbGiqpCIE99FuVPv4PV8J3SHHfKJzNRz9cHo+64zbK59GDIUu/V/zz8L3z0+SydH+HZJ8bo9QsW/YrIqBg8Pe5h/Vp6K0jw4P67R6gAQ2EPHUtF554sWLgEEVHReObxwjpk8RoVUJBeDnIMNzUrzZz5P+JkYhKe+s+/1UilPLz/0Qw95ONfqt70jEx8PONr9OrWGf2uLuyVJXVI8CFTDduY/sVs3Svi+gHnp5o1hldMGPeIbMpCAQpQgAIUoAAFKFCBAqXvx16BB2VV5haQm99GE55Fw8fVNJeRkWVqrO7Wf24PHVSw2ruWGjZhDJ2QvAUVWXTdxUzzWapjqBvzf5pVolR1FLeRqtfeutfGBdsYs0tcysJOZdN3VI9iiwrsWCfKNLYpbqpTY13D8U/BQeWYMI5rLOfvK0/A+ByW9cylZ0O2Gt5gXXJVkFKWG6Wu9/nAXcP6QTh67LjqiZCqh1AY25T1t9ThoP5+SMBBSoP6wYiJjdc9HNxUTwu9LLgwb4uxTZYE81SJiIzGij/XIEn1hjBi7HHxRXtS6A35gwIUoAAFKEABClCgUgQ4vKJSWG2jUgkauDZrVqUnk6G+/c9UPRZYKl5Akngy4FDxrldSjR7ubnoohXHzLj0mZKiCMWxCLIx18lyGPUgOCNlPiuO54ER6emFuB72wFD/86/npHg3Sq0GK5IyQej09zie7rS0JWospP/3yG2R6zxFDb8ED94zUwYsLN5MgjHW7L1zP1xSgAAUoQAEKUIAC5Rco/n9p5a+Pe1aBwKJFizB16tQqOFLVH+Lzzz/HihWlH85R9S3kESlQ8wWmT5+OsLCwMp9It84ddeJGGY5w6Eg4vl2wSN+sd1fJGY0is1Ns37kHO/fs04kdJbeDMfVlsyZX6WCBTJu5Z+8BHDh0FGnp6Xrog8yMIY80FZCQQX/yPDL6hK62bUhL/ft/C37G6nWb9DqZLUOm1y2xqLocVe+tOiq5quSCkMCFJLSU6TKN0qhhfX1e6zZuxf6DR3T9xjr+pgAFKEABClCAAhS4PAEOr7g8v2rZe/PmzTh9LnFbtTSgEg8aEREB6a49ZMiQSjwKq6bAlS0gU+6WJxltqxZN0TWmHbbv+hvHVa4G6W0Q2qMrrmoQbAGVxJJ/rd2AvLx8HRQY0DfUsk56FHTt1B7bwnbj1+V/6uUD+4Widcvm+F7lcrAu8lqCCpIfQoZpNGnUUAcDJFmlo6MDBva/Wm9eNMWsdQ2Fz/v07oFV6zbgm28XwMnJEQ3UMAwjweXYh+7TG7Vq0QxbVQLMdZsKE+ZKEEKOx0IBClCAAhSgAAUocPkCTCR5+YZVXsOCBQsQp6ZYHDduXJUfu7IPOHnyZLRUszcMHjy4sg/F+ilwxQrI345///vfaN/+/Gw0ZcWQHgruboXDJox9JZGki0pgOvreO3SPBQlAFFckIJGq9pceCEYOhuK2u3CZ7Ce5Gsqyj1GHTJ3pqqbulCEhBZJEUwVAJGhiXWQbWSJTfLJQgAIUoAAFKEABClSMAIMOFePIWihAAQrUGIG9e/fqYRFt27at0DZbBx0qtGJWRgEKUIACFKAABShQYwU4vKLGXjo2nAIUoED5BNq0aVO+HUvYS2aScL1wetcS9uFqClCAAhSgAAUoQAHbFmBPB9u+vjw7ClCAAhSgAAUoQAEKUIACFKBAtQmUIvV3tbWNB76EwNy5czFjxoxLrK3Zi9988038/vvvNfsk2HoKmFxg06ZN2L9/v8lbyeZRgAIUoAAFKEABCtiCAIMONfAqys1CeHh4DWx5yU2OjIzErl27St6QW1CAAuUWmDVrVrn35Y4UoAAFKEABClCAAhQoiwBzOpRFyyTbNm/eXCeBM0lzKrQZ9evXR+fOnSu0TlZGAQoUFcjJyUG6mj2ChQIUoAAFKEABClCAApUtwJwOlS1cCfXn5uYiPz8fzs7OlVA7q6QABWxdYP78+Tq416JFixpxqjJN8JkzZ3DffffViPaykRSgAAUoQAEKUIAC5wUYdDhvwWcUoAAFKGAigZ9//hmTJ09GgwYN8O2335qoZWwKBShAAQpQgAIUoEBpBTi8orRS3I4CFKAABapEYOHChZBkl9LDQXp0hYaG4o8//kCrVq0gQ7BYKEABClCAAhSgAAVqjgB7OtSca2Vpqcxc4eHhgTvvvNOyzFaefPTRR+jQoQP69u1rK6fE86CA6QQkEa2dnR0aNWpkqrZ98803mDdvHs6qVoW0aQMnJ2c4OToiRw0pC9uxA/l5ufDx8UVISGsMHz4c7dq1M1X72RgKUIACFKAABShAgYsF2NPhYhPTL0lISDB9G8vbwKNHj/KbzPLilXe/s2eRsjOsyN6ena7AZJ4qT0rK7vMzp9jZ28O9XfsiLrby4oUXXsDTTz9tmqDDtm3b8O677yIrKxvDR4zEoMFD4ODgcBH39q1bEX4sHBvWrdNBV19fX6xevfqi7biAAhSgAAUoQAEKUMA8Agw6mOdalLolnp6eqFu3bqm3r0kburi4oF69ejWpySW2NTY2FoGBgSVuV20bFBQgfMK4IofvtGIt1Ffh55epG3Jdatc+v6wCnuWlpSH+px/gc/0NcA4MqoAaL11FTuJJJK1ZY9mg3k03w65OHcvrvMzMix1Wrrest6UntdV1dHNz+8dTkql5W7du/Y/bVMTKRYsW4b333sP1N96Eh/79CBxVz4ZLlS7dukEeI0begfUq8PDu22/g1ddexysvT7zULlxuwwLHjkfB388XLi7nP8c2fLo8NQpQgAIUoECNFWDQoQZeuoEDB6JA3SjaYpkwYYLNnduff/6JX3/9VQcennnmGQQEBJjy0tWf8AJ8rhkI1KplCTjkpaYg4u03kbal8ObbvXc/NPq/F2Hv6lriORSoaRnDX58Ev9tux6V6TqTt2omE2V+glr0Dgu4ZVWKdl7NBzqkkJK9cjvyUZOTERqOuGsLjaBV0sHd3R4dfV+pDxH47FwnzZl3O4Uy975AhQ0oM7kkCR5kpR4Kcr7zyCqRXQUWXsLAwvPPOO3jk0bG4ZdCtZao+9Oqr8d7kqXjt1ZcxbdoHGD/+iTLtz41rvsDS5X+iaeOrcOO1/Wv+yfAMKEABClCAAjYsUHuSKjZ8fjZ5an5+fiXeMNTUE6+jbgKlt4Mtlfbt26N58+Y4efIk5NtjSYjn5eUFf39/c5ymGl4RN2cWvPpfC1c1hWItNazAKOGTXkbG7jA0ePYluPe6Gonz5yAn4SS8+/QzNrnk77Mq6BD57utwadtB1duy2O2cg4Lh3LotfPpfA7tiutMXu1M5Fzqqm2bfWwajto8fUlb/iXoj7kRtl6LBEzl3eaT9vQcZO7cj8P7R5TyauXdro/IllPQ5k8CEJG48fvy47onw119/6Xwr3t7eFXZy48ePR+/QPrh71L3lqtNX/S0MCAjCwoU/qaBegGmGi5TrZLhTmQW27tgFV/XvRasWTcu8L3egAAUoQAEKUKDqBM7fXVTdMXkkClxxAhJ4kIcMtVijuvh/+umnOgjRr18/9O/f35QJ8XJOJeoeDkHjnobPwGv1NctVgZP4WZ8j74mnkK3OJfbLGfC5dSgSF/6Is3l58Bs+At6hV+PEvDlID9uh90n84Tukrlurn/vdPhKeXboiKy4W0R9MtbwP7JycivSGyMvIwIlvvkKmyrFgr4YS1bvjbnh06FhY34rlSN+7By5NmyFp2VK4tm0P/9tHwNGvcFhO3I8/IGXNX6jt4Qnv/gN0e+zUDAgsZReQ4RUTJ07E448/rqeslDwQzZo1w4ABA/SMEpLQtrxFEkZmqCEtjz72n/JWofeTHg8bN67HZ59/jh49ekACl9VRnnjiCe3StWtXBAcHV0cTynXM5JRUeHmW/Tpu2LwdrVs2g7eXZ7mOWxE7nVUBUxkqxEIBClCAAhSggLkFGHQw9/UptnUrVqzQ/9GS//jbWlmnxmnnqG/I5VtYyYMg/6lsdC7DvmTcz1M3tqmpqXp9SEiI5fSlm/a+fft0j4Lu3btblssN/s6dO9G2bVtcrW5OjPHiS5cuRVRUlD6GrGvSpIneR2ylR4J0K5cbqmHDhunlJ06cwJIlSyC/pafJY489ZjnG+++/j8OHD6Nx48Z47rnnLMtfe+01xMXFqWz7PhgzZoy+EZFzkkSgMp4+OzsbkZGRmDp1qm77rFmzqmQMvaWBJTzJVucqxb1dB8uW7urGP169yomPR54apiDDLrKjjsO9Ry+kbd2MiJefg+uCX+CkejCcVeeXEbYFjipXQ53WbXQd9p5e+redgyPqtGyFArVN4vdz4TVgoOUY8iR6xqc4vWQhPPoORObfu3D0qcfQ7uffYS9uUZE4/ctPyAhuCNeOXZC0ZBHy09PR6JlCewcVpHBpFYLsyOOIfOsVZAy/Cw3HXt6NbZHGVeKL5ORkzJ49W3++5TPwwAMP6KNlqCCMBKqk14FMGWn9Pnv99df1ezkoKAiTJk2ytE6CBfHqOsmwiIcfftjyOdqzZw9kloiUlBT9eXn22Wct+0hQIU3l2ZC8Km+88YZl+auvvopMFSCQz8mNN96okzf+97//hbRr8+bNlu3K8kSmxbzpplvKsssltx00aDA+/vAD3RYJ4lVHGTFiBKQ3iARTpGeT5N2RvxMy5afZyt79h/DHqnXIUp8/+RsrM5l07tAWA/uFqtFVanhVKcq6TVuRqt4rFw5tOB4VgwB/Pz3rSCmquaxN8vML1N/0ixOOXlal3JkCFKAABShAgQoXYNChwkkrv8KtKoN75862ObvAggULdI+AlStXwl51c3/ppZcsoPKfYbnRyVdJDa+99lo1bd75oIPcRMlNkQQYrIMOEkSQG6/Tp0+jd+/elrqOHDmiex2kq5tV6WpuFPnPt9x0yTGsE+3JsV1VHgO5EZSgg3WRb36lngun7wsNDcWxY8f0N8PyH3ujdFOJ8KSNMlOHZO03zkeCLWYqeeqmVIq9h7ulWbXdCp/nqlwPRvG7axTqqWELeSoYtGfYTUhTPRykZ0RBz146L4KH6j4v662LowrEBP/rQb2PBB2KFGWVpnoq+JwLFmTHx2Hf3cORsn0bfPr1t2za9K334Fy/ASKdnZCyYplaXhh08LlmgMpNMQAFZ84gYemviP/qc9R/6GHY/UOCQkul1fxE3vMyfEGCY9bf4Mr7r2XLlvoG0fr9Ks3t1auXfm927FjYE8Q4heuvvx4HDx7UgSzr95/UJQG6oUOH6uESxvby+6677sKBAwf0sArr5ffcc49eLsvksxQdHa1zPXTp0kV/vso6VEg+FwlqmM5NN1dM0KFV6xA4OjniqApMVlfQQYKa8pBgovzd2bBhgwqq3KSn/5UpgOU6lTSkxdq8sp7v3LMPy/5Ypav3VJ/t1i2bIzIqGtvCdiMiMgqj772z1IEHCTCs37QNaervn/R4aNakEf73w8+Qeh954B4dzKis85B68wvyqyS4UZnnwLopQAEKUIACV4IAgw418CrLTaqtJpKUqfzk3EaPHn3RlZGeBDNnzrxouSyYNm1ascvlW+DiinQXL65Ikk55XFikh4LceBVXZOy7PC4sxdUjNyTybfby5cv1jZsEKuQb6YocJ39hO8r7urZ7YZfrfBXMMYrx3F4FH6SngxQ31atAir3qGeIYWB9pu3dahmPoFWX8cUb1UMhLTYar6gkhxck/AHbOLkizCjrIawk4SLH39Nbb6xfqR8ysL/XwiuzIY8Yi5CSdgnOAiWcQOddSCXTde+/F+Q3kZnXw4MH6YTmpc08kACePC0ufPn0gjwuL9OyR7v89e/bUAT7r9TI0QB7WZc6cObpXhAQ7ZehC06ZNdRuvueYa683K9Fx6R0g+BjeVvLOiSrNmLbB9+3aMfvDBMlcpvY0kMCPTdI4bN86y/9dffw3pGSJBXuvPv8y4IUOlGjRooHs4GYGgLVu26ACkVCABB/l7JlN6fvfdd7pHk/RGkR4k1TVDT2bmGSz/c40OKvTo0hH9ru5pCTCsWb8ZG7Zsx6+//4lBN178N1DOaVvYHmzetgOZKqAnf6dPJ6dg7cYtKkBmBz9fH/To2gnX9OmNv/cfxKmk03qZ7FdZ5WzBWUvvtco6BuulAAUoQAEKUODyBRh0uHzDKq9BbgqMYQJVfvBKPmBZvzGt5OZUSPVyc7Jjxw5s2rQJ69ev1z0ubr311mr7Rra0J+XoX5gjIX3fXtS5qpHeLUM9l+JUz88SdNAL5IcKhsmsED71hxcuOjfWOk/dmJSlOKnhGFKyYwuHd8i0mgVZmSqHQ9MSqzmtckckzP0KMhOHe/sOSNm6BSc+er/IfkaPB5ld40ot7upmv6TP2m+//YbFixfrIRQSYHj++ecv6gFRXj/paVTS8ctad4AKDB46dKCsu+ntpbeSDKmSgK51kUCE3FzLMBLrIj01ZOiU9HCSIIxRZNu1a9ciKysLt9xyi+4tJcNRZEjVjBkz9DAYCWxUV1mtAgvSxntGDkWD4MLPmdGWvqE9sGffARw6Gq4WDdS9FyQAIUXWBQX44+Dho2p5BpxUr5L8/BzUVb0b7hoxFO5urno7+dGja0f9MBaER0Riz94DSFE9oYJVss/uar37uela4xMS4V/PVwco/lq7UQ/xaNKoobGr2icNxyOjdTDoqobBcLkgX0e+OhdpCwsFKEABClCAAuYWYNDB3Nen2NbJN2gsNUdAeklI0EG6Xsv4ebnhqwlFega4tO2IuBnTYe/lDZVQA/Ffz4RMm2mvkjQaJX7+PPgOHY7kNav0Io/OXfRvubl369oLSb8sVDkeguBYzx8OKmGd9FDITohHrvomND8jXW+brfJrpKtu/c5qO+kxIcc49fOPcFDDMFK3FOYM8FTDNUoqBeduGh19/VS+iBzV42G13iV9z244qDbXVj0G3Fq11ssSFi1Us3D0hSSt9C5F3SUduyatnzJlSonNlW/tR40apRNGlrhxGTc4rXr7VPTnwFkNsylQY/zLU6QXSXHFyKlx4bonn3zywkX6tfQekYcUCTC+9dZbOgghvUtefPFF3fNEpiCtrhIVHaOHPlwYcDDak5eXr9Z76Jv9z2fN0wEKWRehbvz7hfbEHbcNRm5eLuqoPBUfz/ha5atwKhJwkG1lyEVGRiZCWjXHvO8XISqmMHjo4e6GbTv36MegGwbqoRiz5n2Pa/v3wZr1m5Cjgj4yvGPCuEekGsjMFCtXF07VK8PeJHfDv+4eUSTppQRQnFUSWhYKUIACFKAABcwtwKCDua8PW2cjAjKEoiaWxi+9gvBJExHx0jO6+a4duqLhkxOKnIr8x//I4w/rZd433gqXJoVJOWWB/92jEPXftxH5RuH51711OK5SM1/E//A9Tv0431JPwrxZOv+D9FDwU8kFA+9/EBGTXkT05Lf0NvXueUAPs9AvVE4C6yI5CoxSV82ckaQCHeH/V3hTGDB6LDJ2bUPUO6/BadqnKilmezionAk+w0aq4/9PPxxVQsorLehgeP3T71deeeWfVl/Wujx1g5mZmXVZdVy4c3paerUnbZQ8DpIMV3JpyFAZyS8hCSYlsaQZiuQMyVW9MIorcpN/Rq2TIRI7VHBAPtfy2br/7tvx15oNWK0CA9ILQXomSJGcIwVWuWqMOqVXg/RukKCDBBykjtuH3Iymja9SSYJzsWDRr/hl2R8Y8+Aovcsfq9bq382bNsbho8fUkI1UJJ5K0gGHpup4g2++DvsPHsXvK1dh8dLluO+u241D6d9ODDoU8eALClCAAhSggBkFGHQw41UpoU2HDh3S/+Gz7tZbwi5cTYESBWSmB3lI6bRC3QjIt4tqGspWn3yukzKquweVW+HiTPxB9/0LjZ/5P73fhetlmss2c+arnApJqKXqczj3LW/DsePUjBLnx87rna1+uKru7m3mfqcTTdqrBJ7qDW9ZG/zAaMjDKIF33wN5SKmlbqqavztZt1cPo1D7+Q+7rTCJpFUdDf/zBOr/e4zezv5cm6THw55brzeq5e9KFJD8LHv+LhyqU1GHiY5Ws9EEVV/eDskTIzfY0qNJhlTIFLlmK/X8fHW+hd1/70f7toU9fqSNkqvhTxVY8KnrhZ7dOunAgCz39amLADWU6rbBN2Hq9C+w78AhS9BB1stsQlIyVK6IBYuWYNTIYWoIhLMl54N4NL6qgQ44yHbSW6FN6xY6GCFDK4wiM2C0C2mF/374GaQ3hiS1lKEbI4YN0sGP9Zu26E1PxCXofaWnhsxcIeXs2fL1btE78wcFKEABClCAAlUiwKBDlTBX7EG+/PJLPZUjgw4V63rF1qZuxhu99m7R01cBAutid8FYaut18vzCYEOR9erGQ2arKE+RoRblKdbttX5uXZed+oZUHkaprZ4XcbAKUhjb2MrvsWPH6oSGMq1jdZQOHTpg2bLfK/TQR44cxvDbbqvQOstS2YcffliWzcu0barqxfHtgkWor4IqkuRRehIsWbYS/VUiSAkerNu4FTt2/Y27RwzRgYJLVR7asyv2qsDB0hV/Yffe/Tq3ggyHkMSQMuPE3SOG6V2Tz81cczLxlO7lYOS6sO5VIHkcEtUQKSmbtu5AXPxJnFXPjWEzKanpOgBxXM2Mse/AYR0ol3bLcWVKTaO0aNYEHdsVJqN1c3XR52ZvXxtJagiODK84cOgI0tVwjV7du2DL9jB8/9MSlUdiiA6GSB1Jp1OMqvibAhSgAAUoQAGTCjDoYNIL80/NSlUJuSQ5IQsFKkrAWw1LKGuxc64DGZogvQtsoch5lMehJp67fEOdom4sqyvoIFNISo6DmJgYPZPG5RoePx6BFHWTKtPU2mI5Eh6B5JRUPfxBzk96Akig4OCRcB102LJjpx66EBUT949BB5nWUoZLyJSZ0tNAeg54qWBDB3XT308lizSK5HaQxJESbNi8fafuwSHDH3r3KMzXIttd1aA+ok/EYfJHn6seD/lo06oFHNRnKDgoQG/v5qZmXLnpOixcsgyLf1uhq5ZgQvs2rXH9gL56atGggHq46br+ep38CFF1SGBiYL9Q3dtChnxIcGP4rTdBji+Pud/9hNn/+xGPj3lA55Q4cOgwunfpYKmDTyhAAQpQgAIUMJ9ALZWdW76cYKlBAp999hm6deuGLl3O/wewBjWfTaUABapZ4JFHHtE3/Q0bnp8poKqb9Oijj8LHxw+PP/nUZR96+scf4lj4UXzzzTeXXZcZKziTla2TLbZVQxCCA/0Rq3oV7FE9Bnr36ArpHSA9CU6qPAh9e3fXN/yXew6ffjlHBy9GDL1FBQdy1OwR9mq0VdHeT7L8p19+Q7aaBaZLh3Zo16Zwils5tixzUolkjZKlpgqWUpqkj/JfEhmWITklstR5u7jUMarRvyUvRLqaxleGX5xKStZDPIw8E0U25AsKUIACFKAABUwjYBtfUZqGs2oaMmbMmKo5EI9CAQrYpMCJEydQnbMoCOq9996LSZMmqaSLB9Cy5fkb1rKC79+3D2FhOzBWBTFstdRRs0TcMLCf5fQC1fAEeRhFkjZWZKmtAgznh1ScDx5YH0Omqrzr9iHWiyzPrQMOsrA0wQZjZwk4SJEgx4UBB1kueSHqOhbOACI5KFgoQAEKUIACFDC/QNGvLszfXraQAhSgAAUuQyBXzRwhwyuMm7vLqOqydu3duzc6d+6Mqe9PRnxc+YaLpaohIl9+MQNBalraG2644bLaw53PC7iq3hOpqWnnF/AZBShAAQpQgAIUuAwBBh0uA6+6dp05cya+//776jo8j0sBCtRggcOHD+PBBx+ERzmTdFbkqb/88ssqYBCAZ5+ZgG1bC2coKG39aSq3zQfTpiA1JRkycwRLxQnUV7NDJCWnIC09veIqZU0UoAAFKEABClyxAgw61MBL7+vrq+eCr4FNZ5MpQIFqFggJCcGIESOquRWFh3dxccHHH3+M6667Fq9MfAkvPv9/WL16VYlt27d3L16dNBGRKoGkJKRs3fr89I8l7swNShQwZpNIOHmqxG25AQUoQAEKUIACFChJgIkkSxIy4fqIiAg8+eSTWLhwoQlbxyZRgAIUKLvAqVOnMGXKVKxes1olR3RDoJoesnPnrmoaxrMIDAzGkSOHcPZsAfbt3Ye01BRcddVVeOKJJ8Cpg8tuXZo9MjLPwPWCJI6l2Y/bUIACFKAABShAgQsFGHS4UKSGvJb/oPv4+NSQ1rKZFKCAGQRWrlwJV1dX9OzZ0wzNKbYNGRkZWLVqNZYtW4ZkNQ3m6dNJavYEBzUlZI4OMLRq1UpPjdmhA6dJLBaQCylAAQpQgAIUoIDJBBh0MNkFYXMoQAEKVIZAqsqBIFNlSu8AMwcdKuPcWScFKEABClCAAhSgQPUJMKdD9dlXyJG3bClb8rUKOSgroQAFapzA9OnTMXDgQAYcatyVY4MpQAEKUIACFKBAzRZgT4eaff3wxhtvYJ+ap/7111/n2OYafi3ZfApQgAIUoAAFKEABClCAArYmwKBDDb+i2dnZevrM+Ph4TJgwQZ+NdKOWcdu1a9eu4WfH5lOAAmUVyMrKwu7du/HDDz/Ay8sLL7zwQlmr4PYUoAAFKEABClCAAhSoMAEGHSqMsnorysvLg729vW7E/v378eijj+pEkw0aNMDo0aPx1FNPQaanu+eeezBy5Ei93bFjx/T89nXq1ME111yj95EVctMyatQofcNSt25dvPfee3p7+XH//ffD2dkZkuxt7ty5luXSdXvVqlU62CHHateunV63ePFiLFq0SLdN9g0NDdXLN27ciDlz5kDafd1111mm8Dtx4gTeeecdlTzutJ4Gz/qGSWbsOHPmjD6PKVOmWI799ttv49ChQ5CpRGV7b29vvW7mzJkICwuDu7s7HnroITRv3lwv/+WXX7B27VodlJFjDxgwQC+XHiPz5s1DupqbXqbgGzNmjOUYL730Ek6ePIl69erpXiXGiokTJ0ICPv7+/kWWv/nmm4iMjNTbjx8/3pL085tvvsGePXsg5rfddhs6deqkq5L2LF++HLm5uSpjf2fLNUpMTMQHH3ygjxEYGIhXX33VODTk2NImPz+/Isc22irLpSeMUWRqwYSEBO0kPWOM98u0adNw4MAB7TZ27FjIe0bKggULsGnTJr3d0KFD0atXL71869atep28T3r06KHfU7JCrpmct3HtjCCYrJP3RGZmpj7vqVOnyiJd5HxkNhYPDw99PnINpfz888/aw87ODoMGDcINN9ygl0dFRUGud1paGlq2bAk5V6NIvoJatWqhoKAAM2bMMBbrYJwE4vLz8/Hll19alk+ePBk7d+7U5/f+++9brtHs2bP1Tbu8Dx5++GF9PWQnec9+9NFH+j0rn6Hbb79d1yXvWXlvOjk5oU2bNnjuuecsxxg2bJh+b8ix5f1oFNle6pfrIedqFJk+ct26dfr8xKlFixZ6lbw35D1/9uxZjBs3TpvICnmPyXLxkPesYS7esq20n9NJGrr8TQEKUIACFKAABShQHQIMOlSHehUcU2445ObPzc1N34jJa7npkxsjueGVIjcqslwyxMuNuXGzLjf2ckMkN7QyQ4bcUBslJiYGcpMVEBBguTmVdRLAkBvC+vXr6zqNaewkGCABCgl4yLEk87wUucE32iTtadu2rV4udctxZT8JEnTs2FEvlx87duyABFSkji5duliWy02wsVz2Mc5DbpilvqCgIH0eRtBBbh4lE75kxJc2GIGQvXv3ajO5EZTgwtVXX205hgQFJFgg7ezbt69l+Zo1ayzL+/XrZ1m+evVqfQ5y0yj7GDONyHLxlm+g5UbbCDrIjabcmIq9eBh1ySwlf//9Nw4ePKhvso3lciDj2BLgsW6T0VZZ3qdPH0ub5BhSV0hIiD5noyfM+vXr9c2rTEEoAQcj6CBO8p6Rayc380byQfGWtsr7R85BAg9S5P125MgRhIeHo2HDhpYghazbvn27Pge5dhJUMYrc9Bvn1rhxY3h6eupVu3bt0tfGaKMRxJL3mAQQ5BjyHpNzMYq8BySAIfUY7zNZJ8vlfduoUSM0a9bM2Fy3Vd4f8p6V6yEBNiny/jt8+LB+LddNgjdS5NgSBDHOW95XUmJjY7WP7CfvG+vPi7yXxEWCKcZ7QPaRZSkpKfq4cmyjSN3yeTE+t9JjSYpcByOgIiYS+JMi10F6O0m75P1seOmV/EEBClCAAhSgAAUoQAETCDDoYIKLwCZQgAIUoAAFKEABClCAAhSgAAVsUYCzV9jiVeU5UYACFKAABShAAQpQgAIUoAAFTCDAoIMJLgKbQAEKUIACFKAABShAAQpQgAIUsEUBBh1s8arynChAAQpQgAIUoAAFKEABClCAAiYQYNDBBBeBTaAABShAAQpQgAIUoAAFKEABCtiiAIMOtnhVeU4UoAAFKEABClCAAhSgAAUoQAETCDDoYIKLwCZQgAIUoAAFKEABClCAAhSgAAVsUYBBB1u8qjwnClCAAhSgAAUoQAEKUIACFKCACQQYdDDBRWATKEABClCAAhSgAAUoQAEKUIACtijAoIMtXlWeEwUoQAEKUIACFKAABShAAQpQwAQCDDqY4CKwCRSgAAUoQAEKUIACFKAABShAAVsUYNDBFq8qz4kCFKAABShAAQpQgAIUoAAFKGACAQYdTHAR2AQKUIACFKAABShAAQpQgAIUoIAtCjDoYItXledEAQpQgAIUoAAFKEABClCAAhQwgQCDDia4CGwCBShAAQpQgAIUoAAFKEABClDAFgUYdLDFq8pzogAFKEABClCAAhSgAAUoQAEKmECAQQcTXAQ2gQIUoAAFKEABClCAAhSgAAUoYIsCDDrY4lXlOVGAAhSgAAUoQAEKUIACFKAABUwgwKCDCS4Cm0ABClCAAhSgAAUoQAEKUIACFLBFAQYdbPGq8pwoQAEKUIACFKAABShAAQpQgAImEGDQwQQXgU2gAAUoQAEKUIACFKAABShAAQrYogCDDrZ4VXlOFKAABShAAQpQgAIUoAAFKEABEwgw6GCCi8AmUIACFKAABShAAQpQgAIUoAAFbFGAQQdbvKo8JwpQgAIUoAAFKEABClCAAhSggAkEGHQwwUVgEyhAAQpQgAIUoAAFKEABClCAArYowKCDLV5VnhMFKEABClCAAhSgAAUoQAEKUMAEAgw6mOAisAkUoAAFKEABClCAAhSgAAUoQAFbFGDQwRavKs+JAhSgAAUoQAEKUIACFKAABShgAgEGHUxwEdgEClCAAhSgAAUoQAEKUIACFKCALQow6GCLV5XnRAEKUIACFKAABShAAQpQgAIUMIEAgw4muAhsAgUoQAEKUIACFKAABShAAQpQwBYFGHSwxavKc6IABShAAQpQgAIUoAAFKEABCphAgEEHE1wENoECFKAABShAAQpQgAIUoAAFKGCLAgw62OJV5TlRgAIUoAAFKEABClCAAhSgAAVMIMCggwkuAptAAQpQgAIUoAAFKEABClCAAhSwRQEGHWzxqvKcKEABClCAAhSgAAUoQAEKUIACJhBg0MEEF4FNoAAFKEABClCAAhSgAAUoQAEK2KIAgw62eFV5ThSgAAUoQAEKUIACFKAABShAARMIMOhggovAJlCAAhSgAAUoQAEKUIACFKAABWxRgEEHW7yqPCcKUIACFKAABShAAQpQgAIUoIAJBBh0MMFFYBMoQAEKUIACFKAABShAAQpQgAK2KMCggy1eVZ4TBShAAQpQgAIUoAAFKEABClDABAIMOpjgIrAJFKAABShAAQpQgAIUoAAFKEABWxRg0MEWryrPiQIUoAAFKEABClCAAhSgAAUoYAIBBh1McBHYBApQgAIUoAAFKEABClCAAhSggC0KMOhgi1eV50QBClCAAhSgAAUoQAEKUIACFDCBAIMOJrgIbAIFKEABClCAAhSgAAUoQAEKUMAWBRh0sMWrynOiAAUoQAEKUIACFKAABShAAQqYQIBBBxNcBDaBAhSgAAUoQAEKUIACFKAABShgiwIMOtjiVeU5UYACFKAABShAAQpQgAIUoAAFTCDAoIMJLgKbQAEKUIACFKAABShAAQpQgAIUsEUBBh1s8arynChAAQpQgAIUoAAFKEABClCAAiYQYNDBBBeBTaAABShAAQpQgAIUoAAFKEABCtiiAIMOtnhVeU4UoAAFKEABClCAAhSgAAUoQAETCDDoYIKLwCZQgAIUoAAFKEABClCAAhSgAAVsUYBBB1u8qjwnClCAAhSgAAUoQAEKUIACFKCACQQYdDDBRWATKEABClCAAhSgAAUoQAEKUIACtijAoIMtXlWeEwUoQAEKUIACFKAABShAAQpQwAQCDDqY4CKwCRSgAAUoQAEKUIACFKAABShAAVsUYNDBFq8qz4kCFKAABShAAQpQgAIUoAAFKGACgf8HJUMwXu0O6MoAAAAASUVORK5CYII="
+ }
+ },
+ "cell_type": "markdown",
+ "id": "bb89d3f0-7ade-43a8-a527-4bec45971cf6",
+ "metadata": {},
+ "source": [
+ "# Adaptive RAG using local LLMs\n",
+ "\n",
+ "Adaptive RAG is a strategy for RAG that unites (1) [query analysis](https://blog.langchain.dev/query-construction/) with (2) [active / self-corrective RAG](https://blog.langchain.dev/agentic-rag-with-langgraph/).\n",
+ "\n",
+ "In the [paper](https://arxiv.org/abs/2403.14403), they report query analysis to route across:\n",
+ "\n",
+ "* No Retrieval\n",
+ "* Single-shot RAG\n",
+ "* Iterative RAG\n",
+ "\n",
+ "Let's build on this using LangGraph. \n",
+ "\n",
+ "In our implementation, we will route between:\n",
+ "\n",
+ "* Web search: for questions related to recent events\n",
+ "* Self-corrective RAG: for questions related to our index\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8cece98f-a3ed-417e-8b6a-1754e8f9c42a",
+ "metadata": {},
+ "source": [
+ "## Setup\n",
+ "\n",
+ "First, let's install our required packages and set our API keys"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "88debf5c-6972-415c-b8fb-f65eab203b7a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%capture --no-stderr\n",
+ "%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph tavily-python nomic[local]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2369652a",
+ "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(\"TAVILY_API_KEY\")\n",
+ "_set_env(\"NOMIC_API_KEY\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "aea269f6",
+ "metadata": {},
+ "source": [
+ "
\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": "6a5d4a26-249b-4551-aa13-6c373429618e",
+ "metadata": {},
+ "source": [
+ "### LLMs\n",
+ "\n",
+ "#### Local Embeddings\n",
+ "\n",
+ "You can use `GPT4AllEmbeddings()` from Nomic, which can access use Nomic's recently released [v1](https://blog.nomic.ai/posts/nomic-embed-text-v1) and [v1.5](https://blog.nomic.ai/posts/nomic-embed-matryoshka) embeddings.\n",
+ "\n",
+ "Follow the documentation [here](https://docs.gpt4all.io/gpt4all_python_embedding.html#supported-embedding-models).\n",
+ "\n",
+ "#### Local LLM\n",
+ "\n",
+ "(1) Download [Ollama app](https://ollama.ai/).\n",
+ "\n",
+ "(2) Download a `Mistral` model from various Mistral versions [here](https://ollama.ai/library/mistral) and Mixtral versions [here](https://ollama.ai/library/mixtral) available. Also, try one of the [quantized command-R models](https://ollama.com/library/command-r).\n",
+ "\n",
+ "```\n",
+ "ollama pull mistral\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "af8379bd-7eae-4ba6-b632-12e89eab9920",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Ollama model name\n",
+ "local_llm = \"mistral\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "04718a0c-7a48-4243-97a2-940a0239cc12",
+ "metadata": {},
+ "source": [
+ "## Create Index"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "f9ff6b99-080d-4827-b2cb-f775543d76f5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
+ "from langchain_community.document_loaders import WebBaseLoader\n",
+ "from langchain_community.vectorstores import Chroma\n",
+ "from langchain_nomic.embeddings import NomicEmbeddings\n",
+ "\n",
+ "urls = [\n",
+ " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
+ "]\n",
+ "\n",
+ "docs = [WebBaseLoader(url).load() for url in urls]\n",
+ "docs_list = [item for sublist in docs for item in sublist]\n",
+ "\n",
+ "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
+ " chunk_size=250, chunk_overlap=0\n",
+ ")\n",
+ "doc_splits = text_splitter.split_documents(docs_list)\n",
+ "\n",
+ "# Add to vectorDB\n",
+ "vectorstore = Chroma.from_documents(\n",
+ " documents=doc_splits,\n",
+ " collection_name=\"rag-chroma\",\n",
+ " embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n",
+ ")\n",
+ "retriever = vectorstore.as_retriever()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2f3eb922-27a1-4a72-a727-85fbf5b3daf1",
+ "metadata": {},
+ "source": [
+ "## LLMs\n",
+ "\n",
+ "Note: tested cmd-R on Mac M2 32GB and [latency is ~52 sec for RAG generation](https://smith.langchain.com/public/3998fe48-efc2-4d18-9069-972643d0982d/r)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "7045e064-e666-4aea-9111-6e9d2007f27e",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'datasource': 'vectorstore'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Router\n",
+ "\n",
+ "from langchain.prompts import PromptTemplate\n",
+ "from langchain_community.chat_models import ChatOllama\n",
+ "from langchain_core.output_parsers import JsonOutputParser\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
+ "\n",
+ "prompt = PromptTemplate(\n",
+ " template=\"\"\"You are an expert at routing a user question to a vectorstore or web search. \\n\n",
+ " Use the vectorstore for questions on LLM agents, prompt engineering, and adversarial attacks. \\n\n",
+ " You do not need to be stringent with the keywords in the question related to these topics. \\n\n",
+ " Otherwise, use web-search. Give a binary choice 'web_search' or 'vectorstore' based on the question. \\n\n",
+ " Return the a JSON with a single key 'datasource' and no premable or explanation. \\n\n",
+ " Question to route: {question}\"\"\",\n",
+ " input_variables=[\"question\"],\n",
+ ")\n",
+ "\n",
+ "question_router = prompt | llm | JsonOutputParser()\n",
+ "question = \"llm agent memory\"\n",
+ "docs = retriever.get_relevant_documents(question)\n",
+ "doc_txt = docs[1].page_content\n",
+ "print(question_router.invoke({\"question\": question}))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "813cdcef-8b75-4214-a2ed-b89077b3d287",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'score': 'yes'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Retrieval Grader\n",
+ "\n",
+ "from langchain.prompts import PromptTemplate\n",
+ "from langchain_community.chat_models import ChatOllama\n",
+ "from langchain_core.output_parsers import JsonOutputParser\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
+ "\n",
+ "prompt = PromptTemplate(\n",
+ " template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
+ " Here is the retrieved document: \\n\\n {document} \\n\\n\n",
+ " Here is the user question: {question} \\n\n",
+ " If the document contains keywords related to the user question, grade it as relevant. \\n\n",
+ " It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n",
+ " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n",
+ " Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\"\"\",\n",
+ " input_variables=[\"question\", \"document\"],\n",
+ ")\n",
+ "\n",
+ "retrieval_grader = prompt | llm | JsonOutputParser()\n",
+ "question = \"agent memory\"\n",
+ "docs = retriever.get_relevant_documents(question)\n",
+ "doc_txt = docs[1].page_content\n",
+ "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "aeb8b373-0289-4dec-bd4b-8b2701200301",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " In an LLM-powered autonomous agent system, the Large Language Model (LLM) functions as the agent's brain. The agent has key components including memory, planning, and reflection mechanisms. The memory component is a long-term memory module that records a comprehensive list of agents’ experience in natural language. It includes a memory stream, which is an external database for storing past experiences. The reflection mechanism synthesizes memories into higher-level inferences over time and guides the agent's future behavior.\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Generate\n",
+ "\n",
+ "from langchain import hub\n",
+ "from langchain_community.chat_models import ChatOllama\n",
+ "from langchain_core.output_parsers import StrOutputParser\n",
+ "\n",
+ "# Prompt\n",
+ "prompt = hub.pull(\"rlm/rag-prompt\")\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOllama(model=local_llm, temperature=0)\n",
+ "\n",
+ "\n",
+ "# Post-processing\n",
+ "def format_docs(docs):\n",
+ " return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
+ "\n",
+ "\n",
+ "# Chain\n",
+ "rag_chain = prompt | llm | StrOutputParser()\n",
+ "\n",
+ "# Run\n",
+ "question = \"agent memory\"\n",
+ "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
+ "print(generation)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "38345cff-e2d0-436e-aa09-599522a61eed",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'score': 'yes'}"
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Hallucination Grader\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
+ "\n",
+ "# Prompt\n",
+ "prompt = PromptTemplate(\n",
+ " template=\"\"\"You are a grader assessing whether an answer is grounded in / supported by a set of facts. \\n \n",
+ " Here are the facts:\n",
+ " \\n ------- \\n\n",
+ " {documents} \n",
+ " \\n ------- \\n\n",
+ " Here is the answer: {generation}\n",
+ " Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. \\n\n",
+ " Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n",
+ " input_variables=[\"generation\", \"documents\"],\n",
+ ")\n",
+ "\n",
+ "hallucination_grader = prompt | llm | JsonOutputParser()\n",
+ "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "9771caa1-5542-47c3-8354-aeeafcf51964",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'score': 'yes'}"
+ ]
+ },
+ "execution_count": 10,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Answer Grader\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
+ "\n",
+ "# Prompt\n",
+ "prompt = PromptTemplate(\n",
+ " template=\"\"\"You are a grader assessing whether an answer is useful to resolve a question. \\n \n",
+ " Here is the answer:\n",
+ " \\n ------- \\n\n",
+ " {generation} \n",
+ " \\n ------- \\n\n",
+ " Here is the question: {question}\n",
+ " Give a binary score 'yes' or 'no' to indicate whether the answer is useful to resolve a question. \\n\n",
+ " Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n",
+ " input_variables=[\"generation\", \"question\"],\n",
+ ")\n",
+ "\n",
+ "answer_grader = prompt | llm | JsonOutputParser()\n",
+ "answer_grader.invoke({\"question\": question, \"generation\": generation})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "830ba5f7-9c8d-4c01-83b1-e4d51d40d48f",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "' What is agent memory and how can it be effectively utilized in vector database retrieval?'"
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Question Re-writer\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOllama(model=local_llm, temperature=0)\n",
+ "\n",
+ "# Prompt\n",
+ "re_write_prompt = PromptTemplate(\n",
+ " template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n",
+ " for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n",
+ " Here is the initial question: \\n\\n {question}. Improved question with no preamble: \\n \"\"\",\n",
+ " input_variables=[\"generation\", \"question\"],\n",
+ ")\n",
+ "\n",
+ "question_rewriter = re_write_prompt | llm | StrOutputParser()\n",
+ "question_rewriter.invoke({\"question\": question})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "686c9bb1-5069-45f9-8a7e-cba34fe07dd9",
+ "metadata": {},
+ "source": [
+ "## Web Search Tool"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "6c3c1c70-ff84-41e8-bf72-738ed52f2dde",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "### Search\n",
+ "\n",
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "\n",
+ "web_search_tool = TavilySearchResults(k=3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "630d1751-a20b-4858-b3fd-0312de4f3ad7",
+ "metadata": {},
+ "source": [
+ "# Graph \n",
+ "\n",
+ "Capture the flow in as a graph.\n",
+ "\n",
+ "## Graph state"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "6e09087e-b2a9-437a-abee-129e426df799",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from typing import List\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "\n",
+ "class GraphState(TypedDict):\n",
+ " \"\"\"\n",
+ " Represents the state of our graph.\n",
+ "\n",
+ " Attributes:\n",
+ " question: question\n",
+ " generation: LLM generation\n",
+ " documents: list of documents\n",
+ " \"\"\"\n",
+ "\n",
+ " question: str\n",
+ " generation: str\n",
+ " documents: List[str]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "7c5fa507-77ae-426a-a65f-f518b9525bd0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "### Nodes\n",
+ "\n",
+ "from langchain.schema import Document\n",
+ "\n",
+ "\n",
+ "def retrieve(state):\n",
+ " \"\"\"\n",
+ " Retrieve documents\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, documents, that contains retrieved documents\n",
+ " \"\"\"\n",
+ " print(\"---RETRIEVE---\")\n",
+ " question = state[\"question\"]\n",
+ "\n",
+ " # Retrieval\n",
+ " documents = retriever.get_relevant_documents(question)\n",
+ " return {\"documents\": documents, \"question\": question}\n",
+ "\n",
+ "\n",
+ "def generate(state):\n",
+ " \"\"\"\n",
+ " Generate answer\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, generation, that contains LLM generation\n",
+ " \"\"\"\n",
+ " print(\"---GENERATE---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # RAG generation\n",
+ " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
+ " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
+ "\n",
+ "\n",
+ "def grade_documents(state):\n",
+ " \"\"\"\n",
+ " Determines whether the retrieved documents are relevant to the question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates documents key with only filtered relevant documents\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # Score each doc\n",
+ " filtered_docs = []\n",
+ " for d in documents:\n",
+ " score = retrieval_grader.invoke(\n",
+ " {\"question\": question, \"document\": d.page_content}\n",
+ " )\n",
+ " grade = score[\"score\"]\n",
+ " if grade == \"yes\":\n",
+ " print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
+ " filtered_docs.append(d)\n",
+ " else:\n",
+ " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n",
+ " continue\n",
+ " return {\"documents\": filtered_docs, \"question\": question}\n",
+ "\n",
+ "\n",
+ "def transform_query(state):\n",
+ " \"\"\"\n",
+ " Transform the query to produce a better question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates question key with a re-phrased question\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---TRANSFORM QUERY---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # Re-write question\n",
+ " better_question = question_rewriter.invoke({\"question\": question})\n",
+ " return {\"documents\": documents, \"question\": better_question}\n",
+ "\n",
+ "\n",
+ "def web_search(state):\n",
+ " \"\"\"\n",
+ " Web search based on the re-phrased question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates documents key with appended web results\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---WEB SEARCH---\")\n",
+ " question = state[\"question\"]\n",
+ "\n",
+ " # Web search\n",
+ " docs = web_search_tool.invoke({\"query\": question})\n",
+ " web_results = \"\\n\".join([d[\"content\"] for d in docs])\n",
+ " web_results = Document(page_content=web_results)\n",
+ "\n",
+ " return {\"documents\": web_results, \"question\": question}\n",
+ "\n",
+ "\n",
+ "### Edges ###\n",
+ "\n",
+ "\n",
+ "def route_question(state):\n",
+ " \"\"\"\n",
+ " Route question to web search or RAG.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---ROUTE QUESTION---\")\n",
+ " question = state[\"question\"]\n",
+ " print(question)\n",
+ " source = question_router.invoke({\"question\": question})\n",
+ " print(source)\n",
+ " print(source[\"datasource\"])\n",
+ " if source[\"datasource\"] == \"web_search\":\n",
+ " print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n",
+ " return \"web_search\"\n",
+ " elif source[\"datasource\"] == \"vectorstore\":\n",
+ " print(\"---ROUTE QUESTION TO RAG---\")\n",
+ " return \"vectorstore\"\n",
+ "\n",
+ "\n",
+ "def decide_to_generate(state):\n",
+ " \"\"\"\n",
+ " Determines whether to generate an answer, or re-generate a question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Binary decision for next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---ASSESS GRADED DOCUMENTS---\")\n",
+ " state[\"question\"]\n",
+ " filtered_documents = state[\"documents\"]\n",
+ "\n",
+ " if not filtered_documents:\n",
+ " # All documents have been filtered check_relevance\n",
+ " # We will re-generate a new query\n",
+ " print(\n",
+ " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
+ " )\n",
+ " return \"transform_query\"\n",
+ " else:\n",
+ " # We have relevant documents, so generate answer\n",
+ " print(\"---DECISION: GENERATE---\")\n",
+ " return \"generate\"\n",
+ "\n",
+ "\n",
+ "def grade_generation_v_documents_and_question(state):\n",
+ " \"\"\"\n",
+ " Determines whether the generation is grounded in the document and answers question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Decision for next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---CHECK HALLUCINATIONS---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ " generation = state[\"generation\"]\n",
+ "\n",
+ " score = hallucination_grader.invoke(\n",
+ " {\"documents\": documents, \"generation\": generation}\n",
+ " )\n",
+ " grade = score[\"score\"]\n",
+ "\n",
+ " # Check hallucination\n",
+ " if grade == \"yes\":\n",
+ " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n",
+ " # Check question-answering\n",
+ " print(\"---GRADE GENERATION vs QUESTION---\")\n",
+ " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
+ " grade = score[\"score\"]\n",
+ " if grade == \"yes\":\n",
+ " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
+ " return \"useful\"\n",
+ " else:\n",
+ " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n",
+ " return \"not useful\"\n",
+ " else:\n",
+ " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n",
+ " return \"not supported\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ed7d8eb6-31d7-4ab5-8a88-7081b64582bb",
+ "metadata": {},
+ "source": [
+ "## Build Graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "450eb313-ca75-4a43-b57e-7034bd3f40bf",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langgraph.graph import END, StateGraph, START\n",
+ "\n",
+ "workflow = StateGraph(GraphState)\n",
+ "\n",
+ "# Define the nodes\n",
+ "workflow.add_node(\"web_search\", web_search) # web search\n",
+ "workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
+ "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
+ "workflow.add_node(\"generate\", generate) # generate\n",
+ "workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
+ "\n",
+ "# Build graph\n",
+ "workflow.add_conditional_edges(\n",
+ " START,\n",
+ " route_question,\n",
+ " {\n",
+ " \"web_search\": \"web_search\",\n",
+ " \"vectorstore\": \"retrieve\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"web_search\", \"generate\")\n",
+ "workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"grade_documents\",\n",
+ " decide_to_generate,\n",
+ " {\n",
+ " \"transform_query\": \"transform_query\",\n",
+ " \"generate\": \"generate\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"transform_query\", \"retrieve\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"generate\",\n",
+ " grade_generation_v_documents_and_question,\n",
+ " {\n",
+ " \"not supported\": \"generate\",\n",
+ " \"useful\": END,\n",
+ " \"not useful\": \"transform_query\",\n",
+ " },\n",
+ ")\n",
+ "\n",
+ "# Compile\n",
+ "app = workflow.compile()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "b095c1db-8bd1-4a34-937c-1a9b74ae74ff",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "---ROUTE QUESTION---\n",
+ "What is the AlphaCodium paper about?\n",
+ "{'datasource': 'web_search'}\n",
+ "web_search\n",
+ "---ROUTE QUESTION TO WEB SEARCH---\n",
+ "---WEB SEARCH---\n",
+ "\"Node 'web_search':\"\n",
+ "'\\n---\\n'\n",
+ "---GENERATE---\n",
+ "---CHECK HALLUCINATIONS---\n",
+ "---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\n",
+ "---GRADE GENERATION vs QUESTION---\n",
+ "---DECISION: GENERATION ADDRESSES QUESTION---\n",
+ "\"Node 'generate':\"\n",
+ "'\\n---\\n'\n",
+ "(' The AlphaCodium paper introduces a new approach for code generation by '\n",
+ " 'Large Language Models (LLMs). It presents AlphaCodium, an iterative process '\n",
+ " 'that involves generating additional data to aid the flow, and testing it on '\n",
+ " 'the CodeContests dataset. The results show that AlphaCodium outperforms '\n",
+ " \"DeepMind's AlphaCode and AlphaCode2 without fine-tuning a model. The \"\n",
+ " 'approach includes a pre-processing phase for problem reasoning in natural '\n",
+ " 'language and an iterative code generation phase with runs and fixes against '\n",
+ " 'tests.')\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pprint import pprint\n",
+ "\n",
+ "# Run\n",
+ "inputs = {\"question\": \"What is the AlphaCodium paper about?\"}\n",
+ "for output in app.stream(inputs):\n",
+ " for key, value in output.items():\n",
+ " # Node\n",
+ " pprint(f\"Node '{key}':\")\n",
+ " # Optional: print full state at each node\n",
+ " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
+ " pprint(\"\\n---\\n\")\n",
+ "\n",
+ "# Final generation\n",
+ "pprint(value[\"generation\"])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "644c7293-9cb5-4236-ba08-1e63b0309cb7",
+ "metadata": {},
+ "source": [
+ "Trace: \n",
+ "\n",
+ "https://smith.langchain.com/public/81813813-be53-403c-9877-afcd5786ca2e/r"
+ ]
+ }
+ ],
+ "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.8"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/langgraph/source/examples/rag/langgraph_agentic_rag.ipynb b/langgraph/source/examples/rag/langgraph_agentic_rag.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..268914f5142ab066e2fe08a75c74082dfaf48022
--- /dev/null
+++ b/langgraph/source/examples/rag/langgraph_agentic_rag.ipynb
@@ -0,0 +1,547 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "47e3b43b",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "425fb020-e864-40ce-a31f-8da40c73d14b",
+ "metadata": {},
+ "source": [
+ "# Agentic RAG\n",
+ "\n",
+ "[Retrieval Agents](https://python.langchain.com/v0.2/docs/tutorials/qa_chat_history/#agents) are useful when we want to make decisions about whether to retrieve from an index.\n",
+ "\n",
+ "To implement a retrieval agent, we simple need to give an LLM access to a retriever tool.\n",
+ "\n",
+ "We can incorporate this into [LangGraph](https://langchain-ai.github.io/langgraph/).\n",
+ "\n",
+ "## Setup\n",
+ "\n",
+ "First, let's download the required packages and set our API keys:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "969fb438",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install -U --quiet langchain-community tiktoken langchain-openai langchainhub chromadb langchain langgraph langchain-text-splitters"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "e4958a8c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import getpass\n",
+ "import os\n",
+ "\n",
+ "\n",
+ "def _set_env(key: str):\n",
+ " if key not in os.environ:\n",
+ " os.environ[key] = getpass.getpass(f\"{key}:\")\n",
+ "\n",
+ "\n",
+ "_set_env(\"OPENAI_API_KEY\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3d07e8d4",
+ "metadata": {},
+ "source": [
+ "
\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": "c74e4532",
+ "metadata": {},
+ "source": [
+ "## Retriever\n",
+ "\n",
+ "First, we index 3 blog posts."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "e50c9efe-4abe-42fa-b35a-05eeeede9ec6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langchain_community.document_loaders import WebBaseLoader\n",
+ "from langchain_community.vectorstores import Chroma\n",
+ "from langchain_openai import OpenAIEmbeddings\n",
+ "from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
+ "\n",
+ "urls = [\n",
+ " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
+ "]\n",
+ "\n",
+ "docs = [WebBaseLoader(url).load() for url in urls]\n",
+ "docs_list = [item for sublist in docs for item in sublist]\n",
+ "\n",
+ "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
+ " chunk_size=100, chunk_overlap=50\n",
+ ")\n",
+ "doc_splits = text_splitter.split_documents(docs_list)\n",
+ "\n",
+ "# Add to vectorDB\n",
+ "vectorstore = Chroma.from_documents(\n",
+ " documents=doc_splits,\n",
+ " collection_name=\"rag-chroma\",\n",
+ " embedding=OpenAIEmbeddings(),\n",
+ ")\n",
+ "retriever = vectorstore.as_retriever()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "225d2277-45b2-4ae8-a7d6-62b07fb4a002",
+ "metadata": {},
+ "source": [
+ "Then we create a retriever tool."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "0b97bdd8-d7e3-444d-ac96-5ef4725f9048",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langchain.tools.retriever import create_retriever_tool\n",
+ "\n",
+ "retriever_tool = create_retriever_tool(\n",
+ " retriever,\n",
+ " \"retrieve_blog_posts\",\n",
+ " \"Search and return information about Lilian Weng blog posts on LLM agents, prompt engineering, and adversarial attacks on LLMs.\",\n",
+ ")\n",
+ "\n",
+ "tools = [retriever_tool]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fe6e8f78-1ef7-42ad-b2bf-835ed5850553",
+ "metadata": {},
+ "source": [
+ "## Agent State\n",
+ " \n",
+ "We will define a graph.\n",
+ "\n",
+ "A `state` object that it passes around to each node.\n",
+ "\n",
+ "Our state will be a list of `messages`.\n",
+ "\n",
+ "Each node in our graph will append to it."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "0e378706-47d5-425a-8ba0-57b9acffbd0c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from typing import Annotated, Sequence, TypedDict\n",
+ "\n",
+ "from langchain_core.messages import BaseMessage\n",
+ "\n",
+ "from langgraph.graph.message import add_messages\n",
+ "\n",
+ "\n",
+ "class AgentState(TypedDict):\n",
+ " # The add_messages function defines how an update should be processed\n",
+ " # Default is to replace. add_messages says \"append\"\n",
+ " messages: Annotated[Sequence[BaseMessage], add_messages]"
+ ]
+ },
+ {
+ "attachments": {
+ "7ad1a116-28d7-473f-8cff-5f2efd0bf118.png": {
+ "image/png": "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AQQQQAABBBBAgOAJ9wACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEBIgeBLCoIsAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIEDzhHkAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEQgIET0IYdBFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABgifcAwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBASIDgSQiDLgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCBA8IR7AAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAICRA8CWHQRQABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQInnAPIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIhAYInIQy6CCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggADBE+4BBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQCAkQPAkhEEXAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEECB4wj2AAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCIQECJ6EMOgigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgRPuAcQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgZAAwZMQBl0EEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAgOAJ9wACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEBIgeBLCoIsAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIEDzhHkAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEQgIET0IYdBFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABgifcAwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBASIDgSQiDLgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCBA8IR7AAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAICRA8CWHQRQABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQInnAPIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIhAYInIQy6CCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggADBE+4BBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQCAkQPAkhEEXAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEECB4wj2AAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCIQECJ6EMOgigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgRPuAcQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgZAAwZMQBl0EEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAgOAJ9wACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEBIgeBLCoIsAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIEDzhHkAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEQgIET0IYdBFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABgifcAwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBASIDgSQiDLgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCBA8IR7AAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAICRA8CWHQRQABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQInnAPIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIhAYInIQy6CCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggADBE+4BBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQCAkQPAkhEEXAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEECB4wj2AAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCIQECJ6EMOgigAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgRPuAcQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgZAAwZMQBl0EEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAgOAJ9wACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEBIgeBLCoIsAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIEDzhHkAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEQgIET0IYdBFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABgifcAwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBASIDgSQiDLgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCBA8IR7AAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAICRA8CWHQRQABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQInnAPIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIhAYInIQy6CCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggADBE+4BBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQCAkkD/Up4sAAggggAACCCCAAAIIIIAAArkk8N5779n48ePt0EMPtWeffdby5897f6Lv2LHD7r77btu1a5c1a9bMunbtmks6uXOYjRs32gMPPBD3YHfddZfVrFkz7jJmIoAAAggggMCBL3DI31478C+TK0QAAQQQQAABBBBAAAEEEEBg7wssXbrUPvvsM5sxY4ZNnTrV1qxZY0ceeaT/OvHEE61t27bBSdx00002cOBAf3rmzJlWuHDhYFle6Wzbts3q1avnn85pp51mr7/++l49tVmzZlnPnj3jHuOQQw6xKlWqWO3ate3MM8/0+3FXzMLMFStWWKtWreJu8eWXX1rz5s3jLmMmAggggAACCBz4Annvay0HvjlXiAACCCCAAAIIIIAAAgggcAAKDBo0yLp162abNm2KurqxY8eaXn379rV7773XbrjhBlMggJZeYP369TZy5Mj0C2LmPP744/bwww/nOBOmaNGidsUVVwR7nzRpkk2ePDmYpoMAApkLzF2+yT7+ZbHNWLjRFixLse3bd1mVCkWsfrXidupRFa19w3KZ74ClvsD2nX/bzzNW+/0qZQpb/SrFkEEAgTwgQPAkD3wInAICCCCAAAIIIIAAAggggMD+LfDuu+/aQw89FFyEsjWOPfZYK1u2rI0ZM8Z++eUXf1mfPn2sc+fO/vxgZTpxBRo0aGBVq1YNlimLZ+LEicH0o48+ag0bNrQ2bdoE87LaKVasmPXo0SPYTJk1BE8CDjoIZCrQb/Qie+GzWenWWeQFVPQaOn65de5Uw+44q54XME63GjNCAms3brV7+0zx5xzbpJz16to0tJQuAgjsKwGCJ/tKnuMigAACCCCAAAIIIIAAAggcEALJycn21FNPBddy55132s03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IECgh8MffvihValSJdOzGzRokHXr1s02bdoUtZ6CB3r17dvX7r33Xrvhhhu8bw3vv18brlChQmBTunTpqGuNnVi3bp116dLFn33ppZfa448/HrvKbqezcrzd7mwfr6AH+gpOuTZp0iSbPHmym9wr7wsWLLCRI0cmtO+KFSsmtF5eWil//vx2/vnn+6/777/frrvuOhs9erT/c/jwww/7wcy8dL6cy+4Flq1LtUufHWspm7b7K+c79BBr4gUMWniv1vXLRO3g2ld+t+R/1rvyxJp22lHRgZUXB/1lP/2xxqqVL2zPXdkkats9PbF5606bPHudv9uSRQt4wZOcH+GkphUsJXW7TZidbNPmJJuycXSMi5781d67q5WVLZaU84OwBwQQyJYAwZNssbERAggggAACCCCAAAIIIIDA/iywatUqv26CrkFDEqnmSZky0Q/sYq9PdRYeeuihYHa9evXs2GOPNQ0pNGbMGPvll1/8ZX369LHOnTv784OV97POE0884QeClH1z1VVX7fWzz+3j7c0LKlasWFRWz+uvv77Xgyfh6+nQoYMVKFAgPCuqv79naSg49fbbb5syovRzN378eP+ljDHa/iPw3Fezg8BJMS8I0ffOllajbOG4FzB93nrb/k9NkNe/nZsuePLn4o22YGmKrduwNe72eX1mhZIF7bqTvYy9k81mLUmxa14cb6lekGb1uq32xuB5dv8Fh+f1S+D8EDhgBQieHLAfLReGAAIIIIAAAggggAACCCCQkYACIa5pCKPdBU6Sk5PtqaeecpvYnXfeaTfffLPly5c2pIrqpgwYMMAUBPj000/368CJLlKZOI899lhwvXu7k9vH29vXsy/3/+qrr5oCOAdyS0pKsptuuskPnug6X3vtNb/+yYF8zQfSta1ITrVRk1b6l1TIG5bq4/taW7niBRO6xOWrt9g0b5ivRjVKJLT+/rZS/arFrN+9re2inr/6GSgDf15iN59e10oU5hHu/vZZcr4HhgA/eQfG58hVIIAAAggggAACCCCAAAIIZEHg+++/D9a+8MILg35GHWUPuKG6lImhYElsUwH28847zzTEUGxT8EU1KaZPn24aYkkF2JUBcMEFF5i+SR/bvvrqK39dFa6/8sor/ToPymzR9nXr1rWTTz7ZOnXqFLuZP71r1y6/PoS+ka+6LPXr17czzjgj01okf/31lx/8ibfDs88+26/nEl6mOi9vvvmmPyslJSVYpHNUACm2de3a1cLDRWX1eLH707T28d133/nXqKHDGjZsaM2aNfOv9dBDD43aJHy++owKFixow4YNs59++sm0buPGje0///mPlSpVKmo7TUybNs2/VgXK9NkqG0c1bo4++mirUSPn9Q7SHXAfzFAGx4gRI/wMmeLFi/uGJ510UqZnktF9pmysl19+2f95kZHu1dimnyX9PMycOdNmzZrlZ38dfvjh/s+D6uYk0lRbSP6qyfLjjz+a6qLE+9lLZF+sk7sCb/wwPzjghR2qJxw4cRu9N3yBPX1FYzd5wL1XLVPYTmlT2b79ZakfQHnXu95bvAAKDQEEcl8g/W90uX8OHBEBBBBAAAEEEEAAAQQQQACBXBWYPXu2fzwNcRTvgXnsyajWiWvXX3+966Z7j/fwduLEif7QV3qA75rqo3z88cfWu3dv/8F8gwYN3CL//YcffrCBAwf6GSwlSpSwe+65J1iubT/66CO79dZb7a677grmq7N582a7++67LXy+CqL069cvasixqI28ifnz55sCRPHaEUcckS54snr16rjrz507N+78s846Kyp4ktXjxZ5X//79/doy4flu2DQ9lH/uuecsXKtF9u769MD9pZdeCoqOax96+P7ee+/ZN998Y1WrVg3v1g/OKJgVr6nGy4MPPmiFChWKt3i/mKdMlXBWlU568ODBmQ7Xltl9Juf/+7//869dtV9igycKRqkmkIIesU11iJ5//vl028Sup2kFvU4//fTgc125cuVuaxbF2w/zcl9g9JS0rBMd+ZL21bN8AiMnrrSUzjutWKG0zL/d7WBNyjbrP3qRTfcyVpZ4mSvVKxSxRjVL2CXtaljxDDI6vLIj9vHPi23c7LU2xxtKq3blonZWy8pWr2rx3R3O5q3YZF+MXWozF220lV5tlxoVi1pj73gqLl84KbFzvuL4mn7wRAcbOWUVwZPdqrMCAntHgODJ3nFlrwgggAACCCCAAAIIIIAAAnlUYP369cGZJfItd32j3T3obdeunam4eaJN37D/97//HWStKMukadOm/jf8tUz7vfbaa/0siHh1KvTQv1evXn52Sng7Hf/FF1+0c889189Ecefz1ltvRQVOlDWiwvUKDrgH2m7d8LvqtnTs2DGYtWTJEnMBpmBmqKPsBPdQfMuWLX7x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yodqiVQtGwmkL1gVGc7LnePO6H9W3WyrUykyVJ6Kw//yXKcMXy9d0yzYLqPOmpRttnpd2rBjZUtxb2fkxHwE9rYAmSd7W5j9I4AAAggggAACCCCAAAII5DmBq666yj744AP/vPr06WNnnXVWpt+Cb9KkiZ199tn29ddf+9vceOONdsopp1iHDh1MBdw3b97sP8j//fff7aSTTrJLL73UX++6666zL774wu/36NHDL5CtQMmECRNMx3XtP//5j+tm+/2II47ws030jX5lSdx999128cUXm7IH9G3/1157Le6+FWRRBk44MyWc1aEH/uH6Ii1atLDwkE/a6VFHHWXKAlAwqnPnzta1a1e/4LiOrQLrumYNeaWWneNpO3fM22+/3VxGiI7VvXt3UwBHzrp2NWUWqQh6Tppq0ihQoACKPAcMGGDHHHOMaQg2BU6ef/75YPdarswH3Se1atUK5qsTDjb07NnTX3b44Yfb6tWrbdq0af69ovPdU02ZUZllUimo1L59e79A/LXXXmu9evXyD6179p577jFlEukz/+ijj+KeUnbvM+1MPzfar+6T3r172+DBg+344483BTPlpyLy8+bNs3/9619BYCfuSXgz33777WDRTTfdFPTp5H2BJnVL2i9TVvvBiy/HLrVL2mU9S6yhV3ukcvnCtmzVlmB4rtgrr12lmE2elVak/d0RC+zm0+oGq/QeMj/o16sayVypXSEynNxnPy32z02BGNe+HLPEdaPelQ2jYI2GAJvsFYbXdZ3bqkrUOlmZ+Gj0omD1o+qXDvp0EEAgdwUO8X5pySBmmrsnwtEQQAABBBBAAAEEEEAAAQQQyE2B66+/3lRcW+20006zF154IdNvrycnJ9sjjzwSBEMyOtd27doFgRmt89hjj0UFSmK3u+uuu+zWW2+Nmq2HwQMHDvTnzZw5M+phuOq0tG7d2l92xRVXmIIyro0bN84uvPBCNxn1ruCFy3wIb7dt2zZ/eLGolTOZGD58uB+cCK+iIMAZZ5wRnhXVv/POO81l72TneNqZCzBoe12jyzqIOtA/E8rqUYDANdUnUVBLTeeh8wm3//73v+YygWToskUUcMooQ0JBJNVV0XBurt1www123333ucngXYG3jM739ddf9++/YOVsdHTvPvfccwltqYDaM88846+rYIWCTBkNt+aGt9O7PnfXMrvPwnVN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irglARIgARIggWgToHgSbXS8kARIgARIgARIgARIgARIgARIgARIgATijoARUFatWiV16tSJu4F4qOemTZtqy+PGjQvVQ4YMGfT4zJkzcvPmTds5E9IsU6ZMtjLukAAJkAAJkEB0CVA8iS45XkcCJEACJEACJEACJEACJEACJEACJEACcUwAAkpAQIAmV69UqVIcj8a93SOHSdWqVW3J4E3ryP2SMmVKPVy4cKFuEcYLieJh+fLl0y3fSIAESIAESCAmBCiexIQeryUBEiABEiABEiABEiABEiABEiABEiCBOCZw+PBhSZUqlZw6dUpM6Ko4HpLbum/ZsmWYthIkSCC9evXS8h49esirr74qpUuX1mTxEFxq1aoV5hoWkAAJkAAJkEBUCVA8iSox1icBEiABEiABEiABEiABEiABEiABEiABLyOAvCAIZ3Xnzh3JkSOHXL9+3ctGGPlwIIrAEib8Z7mqcuXKAk8Tx3KIKr1791YPlJ07d+r5ChUqyJQpUyRx4sR6zDcSIAESIAESiAmBf34bxaQVXksCJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACfkIgwRPL/GQunAYJkAAJkAAJkAAJkAAJkAAJkAAJkAAJxGsCL774ophE8mvWrJFs2bL5NQ8sa50/f15Sp04tyZMn9+u5cnIkQAIkQAKxS4CeJ7HLm72RAAmQAAmQAAmQAAmQAAmQAAmQAAmQgMcI/P7771KoUCFt//nnnxeE83LVOnXqJCNHjnS1ulfUQ6ivTJkyUTjxirvBQZAACZCAfxGgeOJf95OzIQESIAESIAESIAESIAESIAESIAESiOcEFi9erAnUgaF27dqydu1al4js2LFDvvjiC1mxYoVL9VmJBEiABEiABPyZAMUTf767nBsJkAAJkAAJWATwBCGShtJIgARIgARIgARIgATiD4E5c+bIc889pxNu0aKFQFCJzHr16qVVhg4dqonnI6vP8yRAAiRAAiTgzwQonvjz3eXcSIAESIAE4jWBZcuWSYECBeSXX37RJw7jNQxOngRIgARIgARIgATiIYHJkydLtWrVdObvvPOOTJs2LUIKr776qhQrVkxDff3vf/+LsC5PkgAJkAAJkIC/E2DCeH+/w5wfCZAACZBAvCJw7949wVOGeK1fv17nHhAQIIcPH45XHDhZEiABEiAB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EaIrPJEQizoQT9AvBEXjSYDrkZPiypUruvCDfjAubGPTwARzhUCA8YVn8JhAPHnk08A4w5tveNdHt9zV8UXWPoQe5IHBE8q4J2CPe4EX7o2joT5CvSGRu73njmM9V48hKEEsAmd8xhz7hOM9PicQsOxDpzm272o9c50rn1FTl1sSIAES8GYCeDAHeczgjWJv+J2NhyLsDWIKwjTSSIAESIAESMAfCVA88ce7yjmRAAmQAAn4PAF74WTmzJlStmzZMHP6+eef9elvnEDcaTyxTyMBEiABEiABEiABEiABdxCApyT+3lywYIF69TlrE96YyHdGIwESIAESIAF/JEDxxB/vKudEAiRAAiTg0wTshZOJEydq7H5nE0LYHPxDW716dc2J4KwOy0iABEiABEiABEiABEggpgTgiYJcVwgja2/wKjQhIu3LuU8CJEACJEAC/kAgrN+8P8yKcyABEiABEiABHyVgL5wgnwmSdDozhOpasmSJnqpbt66zKiwjARIgARIgARIgARIgAbcQaNCggYwbN05z7XXq1MkW+hJhEvv06eOWPtgICZAACZAACXgbAYon3nZHOB4SIAESIIF4S2D06NEycuRInT/24VESniGBNvI/IFFx/fr1w6vGchIgARIgARIgARIgARJwGwEkle/Zs6ccOHBASpcure1CQKGRAAmQAAmQgD8SSOSPk+KcSIAESIAESMDXCMDLZMiQITrs4cOHS506dSKcggmZ0Lhx4wjr8SQJkAAJkAAJkAAJkAAJeIIAQnnBazpBggSeaJ5tkgAJkAAJkECcE2DOkzi/BRwACZAACZBAfCcwYcIE+c9//qMYBg0aJC1btowQyeXLl6VUqVISEhIiixYtktSpU0dYnydJgARIgARIgARIgARIgARIgARIgARIgASiRoCeJ1HjxdokQAIkQAIk4FYCM2bMsAkniBcdmXCCzpMnTy5FihTRsF4UTtx6O9gYCZAACZAACZAACZAACZAACZAACZAACSgBep7wg0ACJEACJEACcURg/vz58u6772rvH3zwgXTu3DmORsJuSYAESIAESIAESIAESIAESIAESIAESIAE7AkwYbw9De6TAAmQAAmQQCwRWLp0qU046dq1K4WTWOLObkiABEiABEiABEiABEiABEiABEiABEjAFQIUT1yhxDokQAIkQAIk4EYCq1atkrff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O27cuFA0MmTIoMdnzpyRmzdv2s7t27dP9zNlymQr4w4JkAAJkAAJkAAJkAAJuJMAPU/cSZNtkQAJkAAJOBKgeOJIhMckQAIkQAIk4GECAQEBAgElV65c2pMvCCjIYVK1alXZs2dPKDrBwcEa0guFCxcu1HMI47VixQrdN142esA3EiABEiABEiABEiABEnADAeN54oam2AQJkAAJkAAJhEuA4km4aHiCBEiABEiABDxHIHHixJpcPSYCyvr16z03QCctt2zZMkwpnvbr1auXlvfo0UNeffVVKV26tCaLh+BSq1atMNewgARIgARIgARIgARIgARiQsCIJ/Q8iQlFXksCJEACJBAZAYonkRHieRIgARIgARLwEIFkyZLJggULbMnj4YHy+PFjl3qbPXu2NGvWTCZPnuxS/ahWMv+IJkz4z58KlStXFniawOzLIar07t1bPVB27typ5ytUqCBTpkwRiEQ0EiABEiABEiABEiABEiABEiABEiABEvA1Agkstf6Jrw2a4yUBEiABEiABfyJw/fp19dg4fvy4Tmv37t22UFjhzfPy5ctSqlQpKVasmAow4dWLzXL8SXH+/HlJnTq1JE+ePDa7Zl8kQAIkQAIkQAIkQALxiECHDh00DG7hwoVl0aJF8WjmnCoJkAAJkEBsEvjncdLY7JV9kQAJkAAJkAAJ2AhAbJg/f76EhIRoGf4JRN6QiCwoKEjatm0rO3bs8Jj3SUT9OzsHbxUkiKdw4owOy0iABEiABEiABEiABNxFgM8Bu4sk2yEBEiABEoiIAMWTiOj8fe727dvStWtXjen+8OFDF64Q+fbbb+Wjjz4SPBlMIwESIAESIIHICKRLl04FlKxZs2pV5A05ffp0hJfVq1dPz8+dOzfCejxJAiRAAiRAAiRAAiRAAv5EwIgnJtSsP82NcyEBEiABEvAeAl4vnkCsQAx480IM9Xbt2skPP/wgrgoZMcW9efNmmTdvnsZu37dvn0vNLV68WCZOnCg3b950qb4nK125ckVj0X/99dee7MZv2541a5by279/v9/OkRMjARLwDgLIJwIhJEuWLDqgihUryuHDh8MdHMJ24bVhwwZZs2ZNuPV4ggRIgARIgARIgARIgAT8iQDFE3+6m5wLCZAACXgvAa8XT+zRlShRQhAX/tdff5V+/fpJw4YN5erVq/ZVPLJftmxZady4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q9/RaOsFFKlAMiOAAALlVEDTYfqk/6eTEEg0gUfenR8JnKQHQYiXbuhlLeulRW3m3OVbbcnKre6atpef3Npq18gJKCgQ46/v2rvPMqz4fweM2pBSOtmwVqr9f/3bmvU3m7ci6N/jU2xnEKRZv3GXPTNmkd16DiPFSomeYhFAAAEEChBg2q4CcLiEAAIIIFBsgYRf8fGTTz6xVq1auY+mAVm5cqVddNFFdthhh7kpPl5//fUIgl78a5op5evRo4fbDhw40M2p7jOdcsopriyNOlHSNFy+fG2/+uorn9XVE76mfb28DyetgeLzXHXVVTZp0iQ76aST7PDDD7cLLrjAHfv8O3bscHO+t2nTxo488khr166dXXnllaZAj0+aFz5vO3Tt008/dec1J7xPK1assIsvvtjat2/vymvdurU98sgjpmDKgSQt6KspyY466ijn+8EHH0QtRvPVaxFfTadyxBFHOGe55/3GhwIIt912m7uufMp/xhln2Pjx4yPl+v4uX748ck7tkMGLL77ozqk+HWuKNT13fZ5//nk75phjrFu3bpF59H0BGinSr18/15fu3bu734V///vf/rLb6lmpTP1+9enTx/VDjg8++GAkX1F+VxRIuvvuu11fNc9/7969rWfPnm5U08yZMyNlsoMAAgggkCPQuHHjyEH4zwF/koCKl2BbFgJrsnbaF9PXuqqrBdNSvTnsqHwDJ9HaN/LrFdFOl4tzHZul2/CbjzKNxFF6/6sVpunNSAgggAACCMRLwL9/IHgSL3HqQQABBCqmQMIHT/RiRS/q69Wr50YcPPPMM6ZRHb/+9a9Nc6ffeuuttmTJEvf0RowY4V7Wa/FZvbjWXO2zZs1y92uEgtKJJ57ojps0aeKO9QJe5ftPeB53BRHOOuss93GZgx/+D2h/rGCD7lVasGCBPf7449aiRQv38vybb74xvaT36eabb7ZXXnnFHfbt29eNilGA4rrrrvNZTMEeJU2VEk4ff/yxO9Q0Y0q7du1yC/BOmDDBlaPylB577DF77rnn3H5Rfig4c/vttztj9Ud2f/3rX/crQgGgs88+2yZPnuyudejQwd2jIMk777wTya8g04UXXuiCWf55KO+MGTPcosDaKvlAT9jVvyzbt2+fy+OPx40b5567TipQcf7557vrMtUIE6V58+a58wsXLrS2bdu64IqCU1dffbVN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gAACCCCAAAIIIIAAAggggAACCCCAQL4CBE/ypeECAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIVEQBgicV8anTZwQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEMhXgOBJvjRcQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgYooQPCkIj51+owAAggggAACCCCAAAIIIIAAAggggAACCCCAAAL5ChA8yZeGCwgggAACCCCAAAIIlH+B7du327XXXmvDhg2zvXv3lv8O00MEEEAAAQQQQAABBBBAIAaBKjHkIQsCCCCAAAIIIIAAAggEAgsWLLATTjghl0W9evWsW7dudtVVV1nPnj1zXUuGgylTpti7777rmjpkyBA75JBD9mv23//+d8vKyrI777zTUlJS9rvOCQQQQAABBBBAAAEEEECgvAkc9N8glbdO0R8EEEAAAQQQQAABBEpDYN68eXbyySdbjRo1rH379q6KGTNmRKoaPXq0de/ePXKcDDs7duywv/zlL1atWjW76667rHLlyvs1u3fv3rZq1SqbPXu26/t+GTiBAAIIIIAAAggggAACCJQzAabtKmcPlO4ggAACCCCAAAIIlL7AgAED7L333nOfWbNm2Xnnnecqfe6556JWvmHDBtu2bVvUa/E4uXHjxnyrSUtLs4cfftjuvffeqIGTfG/M58KePXtszZo1xne08gHiNAIIIIAAAggggAACCCSFAMGTpHhMNBIBBBBAAAEEEEAgUQUyMjLskksucc3TyBSfFDx47bXXrEuXLtajRw+3HThwoP3www8+S8xbldGvXz+X//7777dWrVrZxx9/7NYo0b5Ghijdc8897prOaRqxSZMm2UknnWSHH364XXDBBe7YZQx+XHTRRZG8yq9PeM0TBXz8eY06UVI7/DltNWrFpxUrVtjFF1/sRuQceeSR1rp1a3vkkUdMwRQSAggggAACCCCAAAIIIJBsAqx5kmxPjPYigAACCCCAAAIIJJyARlootW3bNtK2ESNG2G233eaOtRbK2rVrzY9S+eabb6xmzZqRvIXtaF2VhQsXumxad0Vp8eLFtn79erffokULt9V6JRoFM3LkSLc+y+OPP266pvtVpwIq3333nct71FFHWf369d3+O++847bh0SKaxsuPqFF5SoMGDcq15kmVKtn/nNi1a5edc845bmovTWmm/k6YMMEee+wxNx3YlVde6e7nBwIIIIAAAggggAACCCCQLAIET5LlSdFOBBBAAAEEEEAAgYQR0KiMadOm2c8//2zff/+9vfDCC65tfgSIghAPPvigO/fmm2+6kSH79u2zW265xXSsKb808mP48OH24osv5tuv6tWru7wa5bF06VI39ddPP/3k8it4sm7dOrffrl07tz3rrLNMHwU75syZY5dddpndcccdproV2FG7N23aZLVq1bJrrrkmUu8nn3yy37RiCoI89NBDLs+XX37pAiP33Xdf1DVPtOC8Rqf07dvXnn/+eRdgUft0/M9//tMInkSo2UEAAQQQQAABBBBAAIEkESB4kiQPimYigAACCCCAAAIIJI6ARlXoE06nnnqqmxpL5zIzM12gokmTJqbPkiVLXFZNn6XgyaJFi9yxpsnaunWr24/2Q0EPpaZNm7qtgiUagXLuuee6MnzwRFNkRUuDBw92p7UI/BtvvGFbtmyx1NTUaFmLde7HH3909ytY4qf4Ouigg1zARu2VR926dYtVBzcjgAACCCCAAAIIIIAAAvEUIHgST23qQgABBBBAAAEEECgXAh06dLAzzjjD9eXpp592ozY0qkSjNZR8UEOBhOOOO86dC//Q6A8lrRGiT2GpWbNmLotfL+WEE06w22+/PVKPRqZESy1btoycPuaYYyL7Jb2zevVqV+Rdd91l+uRNCtoQPMmrwjECCCCAAAIIIIAAAggksgDBk0R+OrQNAQQQQAABBBBAICEFunbtatdee61rm0aHPProo/bEE0/YAw884M5ptIlPmr4rJSXFH7ptfsGOXJlCB748rVeiAM1hhx3mRrb4dVDCQZLQbSU+ykSjZHyAKFxP8+bN3eGAAQPstNNOC19y+w0bNtzvHCcQQAABBBBAAAEEEEAAgUQWIHiSyE+HtiGAAAIIIIAAAggkvIBGjih4oum4tLaHptDSmiIKaGidEo3KuPrqq61SpUoH3JfGjRu7e7XouxaF98GUSZMmufM+eHHAFRRyoxad1yiasWPH2oUXXrhfbgWTlLQ2yj333GMES/Yj4gQCCCCAAAIIIIAAAggkmQDBkyR7YDQXAQQQQAABBBBAILEE6tWrZ5dffrk999xzbnF0v8j6vffea0OGDLGHH37YNLVX//79LT093QVUnnrqqagjOPLrWaNGjdylWbNmuYXm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"
+ }
+ },
+ "cell_type": "markdown",
+ "id": "dc949d42-8a34-4231-bff0-b8198975e2ce",
+ "metadata": {},
+ "source": [
+ "## Nodes and Edges\n",
+ "\n",
+ "We can lay out an agentic RAG graph like this:\n",
+ "\n",
+ "* The state is a set of messages\n",
+ "* Each node will update (append to) state\n",
+ "* Conditional edges decide which node to visit next\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "278d1d83-dda6-4de4-bf8b-be9965c227fa",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "********************Prompt[rlm/rag-prompt]********************\n",
+ "================================\u001b[1m Human Message \u001b[0m=================================\n",
+ "\n",
+ "You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.\n",
+ "Question: \u001b[33;1m\u001b[1;3m{question}\u001b[0m \n",
+ "Context: \u001b[33;1m\u001b[1;3m{context}\u001b[0m \n",
+ "Answer:\n"
+ ]
+ }
+ ],
+ "source": [
+ "from typing import Annotated, Literal, Sequence, TypedDict\n",
+ "\n",
+ "from langchain import hub\n",
+ "from langchain_core.messages import BaseMessage, HumanMessage\n",
+ "from langchain_core.output_parsers import StrOutputParser\n",
+ "from langchain_core.prompts import PromptTemplate\n",
+ "from langchain_core.pydantic_v1 import BaseModel, Field\n",
+ "from langchain_openai import ChatOpenAI\n",
+ "\n",
+ "from langgraph.prebuilt import tools_condition\n",
+ "\n",
+ "### Edges\n",
+ "\n",
+ "\n",
+ "def grade_documents(state) -> Literal[\"generate\", \"rewrite\"]:\n",
+ " \"\"\"\n",
+ " Determines whether the retrieved documents are relevant to the question.\n",
+ "\n",
+ " Args:\n",
+ " state (messages): The current state\n",
+ "\n",
+ " Returns:\n",
+ " str: A decision for whether the documents are relevant or not\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---CHECK RELEVANCE---\")\n",
+ "\n",
+ " # Data model\n",
+ " class grade(BaseModel):\n",
+ " \"\"\"Binary score for relevance check.\"\"\"\n",
+ "\n",
+ " binary_score: str = Field(description=\"Relevance score 'yes' or 'no'\")\n",
+ "\n",
+ " # LLM\n",
+ " model = ChatOpenAI(temperature=0, model=\"gpt-4o\", streaming=True)\n",
+ "\n",
+ " # LLM with tool and validation\n",
+ " llm_with_tool = model.with_structured_output(grade)\n",
+ "\n",
+ " # Prompt\n",
+ " prompt = PromptTemplate(\n",
+ " template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
+ " Here is the retrieved document: \\n\\n {context} \\n\\n\n",
+ " Here is the user question: {question} \\n\n",
+ " If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n",
+ " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\",\n",
+ " input_variables=[\"context\", \"question\"],\n",
+ " )\n",
+ "\n",
+ " # Chain\n",
+ " chain = prompt | llm_with_tool\n",
+ "\n",
+ " messages = state[\"messages\"]\n",
+ " last_message = messages[-1]\n",
+ "\n",
+ " question = messages[0].content\n",
+ " docs = last_message.content\n",
+ "\n",
+ " scored_result = chain.invoke({\"question\": question, \"context\": docs})\n",
+ "\n",
+ " score = scored_result.binary_score\n",
+ "\n",
+ " if score == \"yes\":\n",
+ " print(\"---DECISION: DOCS RELEVANT---\")\n",
+ " return \"generate\"\n",
+ "\n",
+ " else:\n",
+ " print(\"---DECISION: DOCS NOT RELEVANT---\")\n",
+ " print(score)\n",
+ " return \"rewrite\"\n",
+ "\n",
+ "\n",
+ "### Nodes\n",
+ "\n",
+ "\n",
+ "def agent(state):\n",
+ " \"\"\"\n",
+ " Invokes the agent model to generate a response based on the current state. Given\n",
+ " the question, it will decide to retrieve using the retriever tool, or simply end.\n",
+ "\n",
+ " Args:\n",
+ " state (messages): The current state\n",
+ "\n",
+ " Returns:\n",
+ " dict: The updated state with the agent response appended to messages\n",
+ " \"\"\"\n",
+ " print(\"---CALL AGENT---\")\n",
+ " messages = state[\"messages\"]\n",
+ " model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4-turbo\")\n",
+ " model = model.bind_tools(tools)\n",
+ " response = model.invoke(messages)\n",
+ " # We return a list, because this will get added to the existing list\n",
+ " return {\"messages\": [response]}\n",
+ "\n",
+ "\n",
+ "def rewrite(state):\n",
+ " \"\"\"\n",
+ " Transform the query to produce a better question.\n",
+ "\n",
+ " Args:\n",
+ " state (messages): The current state\n",
+ "\n",
+ " Returns:\n",
+ " dict: The updated state with re-phrased question\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---TRANSFORM QUERY---\")\n",
+ " messages = state[\"messages\"]\n",
+ " question = messages[0].content\n",
+ "\n",
+ " msg = [\n",
+ " HumanMessage(\n",
+ " content=f\"\"\" \\n \n",
+ " Look at the input and try to reason about the underlying semantic intent / meaning. \\n \n",
+ " Here is the initial question:\n",
+ " \\n ------- \\n\n",
+ " {question} \n",
+ " \\n ------- \\n\n",
+ " Formulate an improved question: \"\"\",\n",
+ " )\n",
+ " ]\n",
+ "\n",
+ " # Grader\n",
+ " model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n",
+ " response = model.invoke(msg)\n",
+ " return {\"messages\": [response]}\n",
+ "\n",
+ "\n",
+ "def generate(state):\n",
+ " \"\"\"\n",
+ " Generate answer\n",
+ "\n",
+ " Args:\n",
+ " state (messages): The current state\n",
+ "\n",
+ " Returns:\n",
+ " dict: The updated state with re-phrased question\n",
+ " \"\"\"\n",
+ " print(\"---GENERATE---\")\n",
+ " messages = state[\"messages\"]\n",
+ " question = messages[0].content\n",
+ " last_message = messages[-1]\n",
+ "\n",
+ " docs = last_message.content\n",
+ "\n",
+ " # Prompt\n",
+ " prompt = hub.pull(\"rlm/rag-prompt\")\n",
+ "\n",
+ " # LLM\n",
+ " llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0, streaming=True)\n",
+ "\n",
+ " # Post-processing\n",
+ " def format_docs(docs):\n",
+ " return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
+ "\n",
+ " # Chain\n",
+ " rag_chain = prompt | llm | StrOutputParser()\n",
+ "\n",
+ " # Run\n",
+ " response = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
+ " return {\"messages\": [response]}\n",
+ "\n",
+ "\n",
+ "print(\"*\" * 20 + \"Prompt[rlm/rag-prompt]\" + \"*\" * 20)\n",
+ "prompt = hub.pull(\"rlm/rag-prompt\").pretty_print() # Show what the prompt looks like"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "955882ef-7467-48db-ae51-de441f2fc3a7",
+ "metadata": {},
+ "source": [
+ "## Graph\n",
+ "\n",
+ "* Start with an agent, `call_model`\n",
+ "* Agent make a decision to call a function\n",
+ "* If so, then `action` to call tool (retriever)\n",
+ "* Then call agent with the tool output added to messages (`state`)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "8718a37f-83c2-4f16-9850-e61e0f49c3d4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langgraph.graph import END, StateGraph, START\n",
+ "from langgraph.prebuilt import ToolNode\n",
+ "\n",
+ "# Define a new graph\n",
+ "workflow = StateGraph(AgentState)\n",
+ "\n",
+ "# Define the nodes we will cycle between\n",
+ "workflow.add_node(\"agent\", agent) # agent\n",
+ "retrieve = ToolNode([retriever_tool])\n",
+ "workflow.add_node(\"retrieve\", retrieve) # retrieval\n",
+ "workflow.add_node(\"rewrite\", rewrite) # Re-writing the question\n",
+ "workflow.add_node(\n",
+ " \"generate\", generate\n",
+ ") # Generating a response after we know the documents are relevant\n",
+ "# Call agent node to decide to retrieve or not\n",
+ "workflow.add_edge(START, \"agent\")\n",
+ "\n",
+ "# Decide whether to retrieve\n",
+ "workflow.add_conditional_edges(\n",
+ " \"agent\",\n",
+ " # Assess agent decision\n",
+ " tools_condition,\n",
+ " {\n",
+ " # Translate the condition outputs to nodes in our graph\n",
+ " \"tools\": \"retrieve\",\n",
+ " END: END,\n",
+ " },\n",
+ ")\n",
+ "\n",
+ "# Edges taken after the `action` node is called.\n",
+ "workflow.add_conditional_edges(\n",
+ " \"retrieve\",\n",
+ " # Assess agent decision\n",
+ " grade_documents,\n",
+ ")\n",
+ "workflow.add_edge(\"generate\", END)\n",
+ "workflow.add_edge(\"rewrite\", \"agent\")\n",
+ "\n",
+ "# Compile\n",
+ "graph = workflow.compile()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "7b5a1d35",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/jpeg": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "try:\n",
+ " display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\n",
+ "except Exception:\n",
+ " # This requires some extra dependencies and is optional\n",
+ " pass"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "id": "7649f05a-cb67-490d-b24a-74d41895139a",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "---CALL AGENT---\n",
+ "\"Output from node 'agent':\"\n",
+ "'---'\n",
+ "{ 'messages': [ AIMessage(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_z36oPZN8l1UC6raxrebqc1bH', 'function': {'arguments': '{\"query\":\"types of agent memory\"}', 'name': 'retrieve_blog_posts'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-2bad2518-8187-4d8f-8e23-2b9501becb6f-0', tool_calls=[{'name': 'retrieve_blog_posts', 'args': {'query': 'types of agent memory'}, 'id': 'call_z36oPZN8l1UC6raxrebqc1bH'}])]}\n",
+ "'\\n---\\n'\n",
+ "---CHECK RELEVANCE---\n",
+ "---DECISION: DOCS RELEVANT---\n",
+ "\"Output from node 'retrieve':\"\n",
+ "'---'\n",
+ "{ 'messages': [ ToolMessage(content='Table of Contents\\n\\n\\n\\nAgent System Overview\\n\\nComponent One: Planning\\n\\nTask Decomposition\\n\\nSelf-Reflection\\n\\n\\nComponent Two: Memory\\n\\nTypes of Memory\\n\\nMaximum Inner Product Search (MIPS)\\n\\n\\nComponent Three: Tool Use\\n\\nCase Studies\\n\\nScientific Discovery Agent\\n\\nGenerative Agents Simulation\\n\\nProof-of-Concept Examples\\n\\n\\nChallenges\\n\\nCitation\\n\\nReferences\\n\\nPlanning\\n\\nSubgoal and decomposition: The agent breaks down large tasks into smaller, manageable subgoals, enabling efficient handling of complex tasks.\\nReflection and refinement: The agent can do self-criticism and self-reflection over past actions, learn from mistakes and refine them for future steps, thereby improving the quality of final results.\\n\\n\\nMemory\\n\\nMemory\\n\\nShort-term memory: I would consider all the in-context learning (See Prompt Engineering) as utilizing short-term memory of the model to learn.\\nLong-term memory: This provides the agent with the capability to retain and recall (infinite) information over extended periods, often by leveraging an external vector store and fast retrieval.\\n\\n\\nTool use\\n\\nThe design of generative agents combines LLM with memory, planning and reflection mechanisms to enable agents to behave conditioned on past experience, as well as to interact with other agents.', name='retrieve_blog_posts', id='d815f283-868c-4660-a1c6-5f6e5373ca06', tool_call_id='call_z36oPZN8l1UC6raxrebqc1bH')]}\n",
+ "'\\n---\\n'\n",
+ "---GENERATE---\n",
+ "\"Output from node 'generate':\"\n",
+ "'---'\n",
+ "{ 'messages': [ 'Lilian Weng discusses short-term and long-term memory in '\n",
+ " 'agent systems. Short-term memory is used for in-context '\n",
+ " 'learning, while long-term memory allows agents to retain and '\n",
+ " 'recall information over extended periods.']}\n",
+ "'\\n---\\n'\n"
+ ]
+ }
+ ],
+ "source": [
+ "import pprint\n",
+ "\n",
+ "inputs = {\n",
+ " \"messages\": [\n",
+ " (\"user\", \"What does Lilian Weng say about the types of agent memory?\"),\n",
+ " ]\n",
+ "}\n",
+ "for output in graph.stream(inputs):\n",
+ " for key, value in output.items():\n",
+ " pprint.pprint(f\"Output from node '{key}':\")\n",
+ " pprint.pprint(\"---\")\n",
+ " pprint.pprint(value, indent=2, width=80, depth=None)\n",
+ " pprint.pprint(\"\\n---\\n\")"
+ ]
+ }
+ ],
+ "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.2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/langgraph/source/examples/rag/langgraph_crag.ipynb b/langgraph/source/examples/rag/langgraph_crag.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..e039ffa3186bb22fb97aa6c82e5438ee365c8c81
--- /dev/null
+++ b/langgraph/source/examples/rag/langgraph_crag.ipynb
@@ -0,0 +1,709 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "c71da2ea",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
+ ]
+ },
+ {
+ "attachments": {
+ "683fae34-980f-43f0-a9c2-9894bebd9157.png": {
+ "image/png": "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eGlmOlBpeGVsWERpbWVuc2lvbj4xNjk4PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6VXNlckNvbW1lbnQ+U2NyZWVuc2hvdDwvZXhpZjpVc2VyQ29tbWVudD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cnve9usAAEAASURBVHgB7N0HeBTV+sfxNwmE3pEqSBFEQKSIggiKKBYUwYYFBRWvYMHutfeOvaF/wXIV4V71othBEBAVpVoQRZAiSBMUQgsl+e9vuBNnJ5tkN8kmW77nefbZ2annfGY2mZ33lJTsQDISAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAsUskFrM+2N3CCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCDgCBKK4EBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBKIiQCAqKqzsFAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAgEAU1wACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBUBAhERYWVnSKAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCBCI4hpAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBCIigCBqKiwslMEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAECUVwDCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACUREgEBUVVnaKAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCBAIIprAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAICoCBKKiwspOEUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEECERxDSCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCERFgEBUVFjZKQIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAIEorgEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIGoCBCIigorO0UAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEECAQxTWAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCAQFQECUVFhZacIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIEorgGEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEoiJAICoqrOwUAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEECAQBTXAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQFQECERFhZWdIoAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIEIjiGkAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEIiKAIGoqLCyUwQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQJRXAMIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAJRESAQFRVWdooAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIEAgimsAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAgKgIEoqLCyk4RQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQIRHENIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIREWAQFRUWNkpAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAgSiuAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgagIEIiKCis7RQABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQIBDFNYAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBAVAQJRUWFlpwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgSiuAYQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSiIkAgKiqs7BQBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQIBAFNcAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAVAQIREWFlZ0igAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggQiOIaQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQiIoAgaiosLJTBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABAlFcAwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAlERIBAVFVZ2igACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggQCCKawABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQCAqAgSiosLKThFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBAhEcQ0ggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAghERYBAVFRY2SkCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggACBKK4BBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBqAgQiIoKKztFAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAgEMU1gAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEBUBAlFRYWWnCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACBKK4BhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBKIiQCAqKqzsFAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAgEAU1wACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBUBAhERYWVnSKAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCBCI4hpAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBCIigCBqKiwslMEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAECUVwDCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACUREgEBUVVnaKAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCBAIIprAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAICoCBKKiwspOEUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEECERxDSCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCERFgEBUVFjZKQIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAIEorgEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIGoCBCIigorO0UAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEECAQxTWAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCAQFQECUVFhZacIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIEorgGEEAAAQQQQAABBEpMYPDgwfbIiBEldjwOhAACCCCA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PGfOHLe0QCU0b948wVUVKVLErcyUKVPc0pAABRWUaaYULlxYUqZMaSZJnjx5XPaxE1+/YS3mixQrVswlGxRDjz76qBoYczkQhB0nV23lypULwplYpSYQ6nuoZMmSUrx4cX16n9a8h3zCpDLxHvKdFXOSQCAIQFH+1FNPyaeffqriQsHq05sCadq0adK/f391alg8DRkyxGoGLKa1Wz7UQyEBEiABEiABEiABEiABEiCBQBCgIioQFFkHCZBATBO499573eInvP/++3LlypWgcMmcObMgmHhCBG557BZN48aNk/fee8+lmgMHDsiDDz7okoadnj17uqXBXZ3dkuXVV1+V1atXu+W9du2aGhzzxfIEhe+55x6pU6eOSz1r165VLtU2bNggqC8YsmfPHjcLswYNGgjcuVGCRyDU9xB60rlz5wR1iPeQb7h4D/nGiblIINAEnn76aSlTpky81U6dOlWef/55lQ8WTwMHDnQpA5d8Z86cUdbXdutkl4zcIQESIAESIAESIAESIAESIIEEEGCMqATAYlYSIAEScCKQJk0aadq0qWBwR8vvv/8uCxYskGbNmumkgK4xS3n27Nk+15kqVSrlggczpk3BLGhYRhUtWlQNPDnFyoE1VMOGDc1iahuu73r16uUWP6JTp04qf+nSpQVsjh07JosWLZJ9+/a51eEtYfDgwSqwupkHcaPgVg2DYyVKlBC4B0yXLp389ddf8ueff8revXtVvK7t27f7pTwaO3aseTq1jWtLCS6BpLiHoFh6+eWX3dwweuop7yFPZFzTeQ+58uAeCYQTgY8++siKt9i9e3fLKkq3Ee51tTVU69atdTLXJEACJEACJEACJEACJEACJJBoAlREJRohKyABEiABEQzomIooMIHSBwoTu9VQIHjdddddAgURAor7Ko0aNVIWUDqQuS6H2BJYPMmLL74oN9xwg+PhmjVrKhdn9jqheMJiF7hDs+e159H7sOAaPny49O3bVydZayi1vCm2Dh48mGBF1Pjx45Xy0DpJ3AbaW6tWLTOJ20EiEOp76MYbb5QHHnhAJk2a5HOPeA95R8V7yDsfHiWBpCSASScDBgxQTXjiiScc/1sXLlyoJo/A6hqTPSgkQAIkQAIkQAIkQAIkQAIkECgCdM0XKJKshwRIIKYJ5M2bV3r37u3GoHHjxm7xltwy+ZnQpUuXBJVMkSKFzJo1S3yd5ZwlSxaVv3r16h7PA6soBDyHQio+ueOOO9xmX8dXBm3FwFhClXmIceGrnD9/XrkmQnB3u7zyyiselXD2vNxPHIGkuIec3FB66wXvIWc6vIecuTCVBMKFwIcffmgpoWDJ7DTBA23F/y2kVatWas0PEiABEiABEiABEiABEiABEggUASqiAkWS9ZAACcQ8gUceeUTKli3rxgFxG6CQgkscuIw7evSoXL161S0fEuBmzleBdYaTpE2b1ilZpaF+WBmNHj3ao3IHlkgYhEI8p7vvvttjXfoA4ich1hQGthAw3Unuu+8+Qdwsf+JNlCxZUubNmyf/+9//pFy5cm7xuJzOd/bsWadklXbp0iWBourbb7+VN954QypXriyTJ092y//444/7FG/DrSAT/CYQiHsoffr0Pp+/SJEijt9xb/ch7yER3kM+f8WYkQSSnAD+31566SXVjj59+jhOmsFBxHeE+9t69eqpOI1J3nA2gARIgARIgARIgARIgARIIKoIJIsL+B6ciO9RhYmdIQESIAHfCJw+fVq5+9q9e7fXAp9//rkUKlTIa55QHLx8+bJyy4eYVqlTpxYMzEOxlBhBkPM9e/bIhQsXBO7Pbr31VsmaNauq8sqVK+p8GMzXC6xMEionT55U9eBcOA9iDGXIkEFy5cqlzpU8ued5FhiUswdnt5+/ffv2AmsoWHxRQkuA95CoeG28h0L7vePZSCAaCUycOFH9l6Fv/fr1E0yw8CTPPPOMsoLGpBG6pPVEiekkQAIkQAIkQAIkQAIkQAL+Ekj46J+/Z2I5EiABEogBAlDizJgxQyk65s+f77HHsIoKB0UUlEBoRyDbctNNNzlamQAGYk35YxVlB3nzzTcLFn/k8OHDXov1799funXrRiWUV0rBO8h7SIT3UPC+X6yZBGKFwDvvvCOvv/666u7zzz8vjz76qMeub9myRSmhoICiEsojJh4gARIgARIgARIgARIgARJIBAHPU8YTUSmLkgAJkEAsE4CCBK7vEJOhUqVKjiiOHDnimM7E4BM4dOiQ40maN2+uXBN1796dcaEcCYUukfdQ6Fj7cybeQ/5QYxkSCB2BUaNGWUqoAQMGeFVCoVVTp05VjWvbtm3oGskzkQAJkAAJkAAJkAAJkAAJxBQBWkTF1OVmZ0mABEJJoHr16oLljz/+kA0bNgjc32Ebrsfy5MkTyqbwXAaBChUqKLeAWbJkkWzZskn+/PmlYsWK4i22llGcmyEkwHsohLATcCreQwmAxawkEGICiH04ZswYddZBgwZJ586dvbbg+++/l9mzZ6u4kYjnSCEBEiABEiABEiABEiABEiCBYBBgjKhgUGWdJEACJEACJEACJEACJEACJBBCAkOHDpXx48erMw4ZMkQ6duwY79kRN2rRokUSn/u+eCtiBhIgARIgARIgARIgARIgARLwQoAWUV7g8BAJkAAJkAAJkAAJkAAJkAAJhDuBwYMHy6RJk1QzoZDyxc3evn37lBIqR44c0qxZs3DvIttHAiRAAiRAAiRAAiRAAiQQwQSoiIrgi8emkwAJkAAJkAAJkAAJkAAJxDYBxIGaMmWKgjB8+HBp3bq1T0Defvttla9NmzbKVa1PhZiJBEiABEiABEiABEiABEiABPwgQEWUH9BYhARIgARIgARIgARIgARIgASSmsBzzz0nM2bMUM148803pUWLFj436cKFC1KkSBGfrKd8rpQZSYAESIAESIAESIAESIAESMCBAGNEOUBhEgmQAAmQAAmQAAmQAAmQAAmEM4FnnnlGZs2apZo4evRoadKkSTg3l20jARIgARIgARIgARIgARKIYQK0iIrhi8+ukwAJkAAJkAAJkAAJkAAJRB6BJ598UubPn68aPnbsWGnYsGHkdYItJgESIAESIAESIAESIAESiBkCVETF++ylXgAAQABJREFUzKVmR0mABEiABEiABEiABEiABCKdQI8ePWTJkiWqG+PHj5f69etHepfYfhIgARIgARIgARIgARIggSgnQEVUlF9gdo8ESIAESIAESIAESIAESCA6CHTr1k1WrFihOjNx4kSpU6dOdHSMvSABEiABEiABEiABEiABEohqAlRERfXlZedIgARIgARIgARIgARIgASigUCnTp1k9erVqisffPCB1KxZMxq6xT6QAAmQAAmQAAmQAAmQAAnEAAEqomLgIrOLJEACJEACJEACJEACJEACkUugbdu2sn79etWBKVOmyL333hu5nWHLSYAESIAESIAESIAESIAEYo4AFVExd8nZYRIgARIgARIgARIgARIggUgh0KpVK9m4caNq7vTp06Vy5cqR0nS2kwRIgARIgARIgARIgARIgAQUASqi+EUgARIgARIgARIgARIgARIggTAk0KRJE9m6datq2cyZM6VixYph2Eo2iQRIgARIgARIgARIgARIgAS8E6AiyjsfHiUBEiABEiABEiABEiABEiCBkBOoV6+e7N69W533k08+kQoVKoS8DTwhCZAACZAACZAACZAACZAACQSCABVRgaDIOkiABEiABEiABEiABEiABEggQARq1Kgh+/btU7XNnj1bypcvH6CaWQ0JkAAJkAAJkAAJkAAJkAAJhJ4AFVGhZ84zkgAJkAAJkAAJkAAJkAAJkIAjAcSAOnjwoDo2d+5cueuuuxzzMZEESIAESIAESIAESIAESIAEIoUAFVGRcqXYThIgARIgARIgARIgARIggagmgBhQhw8fVn389NNPpUyZMlHdX3aOBEiABEiABEiABEiABEggNghQERUb15m9JAESIAESIAESIAESIAESCGMCiAF15MgR1cL58+dL6dKlw7i1bBoJkAAJkAAJkAAJkAAJkAAJ+E6AiijfWTEnCZAACZAACZAACZAACZAACQScQLly5eSPP/5Q9S5cuFBKliwZ8HOwQhIgARIgARIgARIgARIgARJIKgJURCUVeZ6XBEiABEiABEiABEiABEggpglcvXpVoIQ6fvy44rB48WIpXrx4TDNh50mABEiABEiABEiABEiABKKPQPLo6xJ7RAIkQAIkQAIkQAIkQAIkQALhTeD8+fNyV9mylhJq6dKlVEKF9yVj60iABEiABEiABEiABEiABPwkQEWUn+BYjARIgARIgARIgARIgARIgAT8IXD69GlBTKgTJ0+q4suWLZM777zTn6pYhgRIgARIgARIgARIgARIgATCngAVUWF/idhAEiABEiABEiABEiABEiCBaCFw7NgxqVSpkkAZBVm5cqXccccd0dI99oMESIAESIAESIAESIAESIAE3AhQEeWGhAkkQAIkQAIkQAIkQAIkQAIkEHgCBw8elBo1asi5c+fkhhtukFWrVkmRIkUCfyLWSAIkQAIkQAIkQAIkQAIkQAJhRICKqDC6GGwKCZAACZAACZAACZAACZBAdBL4+eefpVatWkoJlTp1amUJVbhw4ejsLHtFAiRAAiRAAlFGoESJElKgQAErtmOUdY/dIQESIIGgE6AiKuiIeQISIAESIAESIAESIAESIIFYJrB7926pX7++XLhwQdKlSyfLly+XQoUKxTIS9p0ESIAESIAEIoLAV199JRMmTFATSa5evSpLly6NiHazkSRAAiQQbgSoiAq3K8L2kAAJkAAJkAAJkAAJkAAJRA2BrVu3SsOGDeXixYuSIUMGNYCFGdUUEiABEiABEiCB8Cfw1ltvyciRIwVKKAgVUeF/zdhCEiCB8CRARVR4Xhe2igRIgARIgARIgARIgARIIMIJfPPNN9KsWTO5fPmyZMqUSZYsWSL58+eP8F6x+SRAAiRAAiQQOwSSJUsmf//9t+pw8uTJZd26dbJly5bYAcCekgAJkECACFARFSCQrIYESIAESIAESIAESIAESIAENIG1a9dK69at1QzqLFmyyOLFi+XWW2/Vh7kmARIgARIgARKIAALHjx+3WnnDDTeobUwsoZAACZAACSSMABVRCePF3CRAAiRAAiRAAiRAAiRAAiEmcPDgQfn666/l+++/D/GZ/TvdqlWrpH379qpwjhw5ZOHChZInTx7/KmMpEiABEiABEiCBJCNw4sQJ69w33nij2oZ7PsR9pJAACZAACfhOgIoo31kxJwmQAAmQAAmQAAmQAAmQQAgJQPFUv359qVy5sjzwwAPWNuI1QDkVjoJZ0g899JBqWq5cuWTevHmSO3fucGwq20QCJEACJEACJOCFwC+//CKmIgoWUTVr1pQDBw4wVpQXbjxEAiRAAk4EqIhyosI0EiABEiABEiABEiABEiCBJCUAZROUUHYrKCigEDQcyqlnnnlGzpw5k6TtNE8OpVOPHj1UUr58+ZQSCsooCgmQAAmQAAmQQOQRWLZsmXKxq1ueMmVKefDBB9UurKIoJEACJEACvhOgIsp3VsxJAiRAAiRAAiRAAiRAAiQQAgJQMEHZpKVr167y8ccfy2+//aZmIN9zzz3q0KxZs5RCCm77klo++eQTeeqpp1QzChYsKLNnzxa45aOQAAmQAAmQAAlEJgG7sil58uRSvnx5yZAhgyxfvlz27t0bmR1jq0mABEggCQhQEZUE0HlKEiABEiABEiCB2CLw66+/qkH1H3/8MbY6zt6SgB8EoISCgglyxx13KMXTwIEDRSuf7rzzTqWUeumll9RAECyi4Lbv/fff9+NsgSkCJVnfvn1VZYULF1btz5YtW2AqZy0kQAIkQAIkQAIhJ7By5UrZtm2by3lhEZU5c2apVq2aSoc7XgoJkAAJkIBvBJJdixPfsjIXCZAACZBArBOYPn26nDx5Ug38IVArFswGK1mypFrHOh/2nwQ0gX379sl3330nmzZtko0bNwr2IUWKFJEsWbLobGrdokULZdFB910uWLgTowRgBQWXfJCWLVvKiBEjvJKA277evXvL7t27Vb5WrVrJG2+84bVMoA/iv7F///6qWijOpk2b5nafB/qcrI8ESIAESIAESCC4BHr27CkLFiyQFClSyOXLl9XJChQoIKtXr5aZM2fKs88+q57tobCikAAJkAAJxE+Aiqj4GTEHCZAACcQcgSNHjggsOM6dO6cWzDbH9rvvviunTp1y5NG4cWOpVKmSVKxYUfLnz++Yh4kkEM0E4Bps/fr18tVXX8m3337rV1fvqVBJit5eWO69916pVauWX3WwEAlEKgFYQcEaCgIXd08//bRPXcF/1KBBg5QrPBQIpTJq6tSp8sILL6h2Fi9eXLB/8803+9RuZiIBEiABEiABEghPAvBiUKdOHRUfKmfOnPL777+rhsLqedWqVXL48GGpWrWqUlBNmDBB6tatG54dYatIgARIIIwIUBEVRheDTSEBEiCBUBPA4B0GzTdv3qwUT4i98csvv8iFCxcS1ZQKFSoohRRcJdHKI1EoWTiMCfz999/qRXTDhg2CBfdPIOXeqvdJpy5tqZAKJFTWFbYETCUULJqgTEqoTJo0SQYPHqyKhUIZNWXKFBkwYIA6X+nSpQX7GTNmTGizmZ8ESIAESIAESCDMCLz55psyatQo1apSpUpZLvrgHljHjerSpYt8/vnn0qxZM8uaO8y6weaQAAmQQFgRoCIqrC4HG0MCJEACwSewa9cuWbFihcB6I9jB3eE/u3Xr1ip2BwK3U0ggGgjAV/zixYvVcvDgQa9dypWjgJQsVknuKlld0qRJ55Y3VZpk8uepPTJ85GDp1q2boD79cqszF7uzhLTv0Fbatm2rk7gmgagiAPd6mLiAyRH+KqE0EAQO79Onj5w9ezaollGTJ08WxK2ClC1bVimh4KqWQgIkQAIkQAIkENkE4Iavdu3ayrU2/uMzZcqkFE7oVYkSJWTRokWqg4hNCYvsNGnSqMlpefPmjeyOs/UkQAIkEGQCKYJcP6snARIgARIIEwJz584VLGvXrvXYIjxEw+81HqJz586trJny5MljrW+55Rar7BdffCE//fST/Pzzz9b6xIkT1nFsYH/8+PHy4YcfWgqpYsWKueThDglECgFYbEABhe++NylUoISUK1VdKaDuLHKXt6zqWMoDGeS2giXlpz3HZUC/MXFLMtm3f4us+2qVetHd9f0OFX9m6kfTqZCKlyYzRBoB3FewYgqEEgp9h2sc/IdhEgTqxn+Zry7+fGVnWl7dfffdAqUUYiZSSIAESIAESIAEIp8A4kLp+K4NGzZUrrd1r1KmTKk3pUqVKmob3kTwftCxY0frGDdIgARIgATcCdAiyp0JU0iABEgg6gi8+uqrAt/VpiCgOuI5FS1aVPLly6cUUDly5DCzJHgbrsmg6FqzZo16YEdcKbskJO6HvSz3SSApCCAA8XvvvefVgjBfniJStlQNKV+mhtxRuGxAmpk+YzLJeEty+WzNHJkzZ65s+u5rVW+ZMmVlyJDBakZmQE7ESkggiQgEwh2fp6bDurBevXrKMiqxVlbmOSZOnCivvPKKSsJ/KGZDp0vnbu1oluE2CZAACZAACZBA5BDQLvdgCQVPIs8//7yyeEIPypUrF/dcPsfqTPPmzeW7775T8aTwjEAhARIgARLwTICKKM9seIQESIAEIp7A3r17pXfv3rJjxw7Vlw4dOgjiN2EGd/bs2YPaP8TPWb9+vaxevVo9rJ8/f946HwbvoJC65557rDRukEC4EUCQ4rFjx8q8efMcm5Ylc065p1xdKV+6hpQqVtExTyASk98gctPNyWXHD1/LxIljZMfub1S177zzjjRo0CAQp2AdJBByAm+99ZaMHDlSnTeQiiKzI3A/C5d/kEAEEn/33XfltddeU/UhQDkU1LAkppAACZAACZAACUQHAbjgbty4sepM+/btBRM6H374YcHENAjeXz/++GO1jY8RI0bI6NGjJW3atCpmLFzTU0iABEiABJwJ3PBynDgfYioJkAAJkEAkE8DDMoK1Hzt2TKpXry4zZsyQJk2aKAuo9OnTB71rcFtQqFAhqVWrlsClwU033aTi3yBuB2aqz549W7UBSikKCYQbgQMHDkiPHj0cXVkWzFdMmjV4RLp3HiIV77pPcmQLrj/4a9dELvx9TTKmyy01qjSXK1evyPc/fKvcBMLqEPfQDTfEaasoJBAhBJ555hmBeztIsJRQqBuuZfHf8+WXX6oF/4VZs2bFoQTLuHHjZOjQoaoc6kH7U6dOneB6WIAESIAESIAESCB8CcDSGRZOEFhCwd3vwoULlTt6pMGTSIsWLbCpJHny5Oq9FnGl4Gnkzjvv1Ie4JgESIAESsBFIbtvnLgmQAAmQQBQQmD59upq5ha7069dPxWjKlStXkvUsf/78Knj8smXLVEBX/YCOGfGPPfZYkrWLJyYBJwJHjx6VJ598Unbv3u1yuHTxKvJU9zdlxOB50qhuJ8lwYwaX46HaadfiKen7xBh1OlhkwOJjw4YNoTo9z0MCiSIAJRRc8kGCqYTSjXzooYekZcuWKgYV7pXvv/9eH/J5/fbbb8uwYcNU/vvuu08poVKlSuVzeWaMXgKIIYJA9lhooRq915k9IwESiA0C8OiBeLCQypUrq8le2E6WLBlWSswYUUiAhRRiLEO8xWJWGfhBAiRAAjFOgIqoGP8CsPskQALRR2DmzJnSv39/1bGPPvpIHn/88bDpJGamd+7cWaZOnaqstdAwPOxTGRU2lyjmG3Lq1CmlhNq8ebPFIvPNOeSJh4bJwL7vy70VG1rpSblRqXxdSxmFtrZp00b5sE/KNvHcJBAfgVAroXR7Bg4cKIiLeObMGaW4TYgyasyYMTJ8+HBVVd26dZWLvxQpUuiqY3IN5V61atWsZcqUKTHJAZ3GDPjjx49bS8yCYMdJgARIIAoI4L0Unjsg8CTiJLCAsgs8gECoiLKT4T4JkAAJuBJw/wV1Pc49EiABEiCBCCLw1VdfybPPPqta3KlTJzVIFI7Nz5Ili5oJP2jQIBVfg8qocLxKsdcmxDHr1auXIK6Mlsp33y+Dn/tIalVrrpPCZm0qo9Conj17KvdjYdNANoQEDAJaCZUhQwZZunSpNRnByBK0TUyC+OSTT1yUUdoqy9tJYbULqy0IrF0QIyrW3WDC3e+qVavkt99+s5b58+d7w8hjJEACJEACJBARBLQ1FCycmjZtarXZtIhymoxSp04dlffPP/9U8ZGtgtwgARIgARJwIUBFlAsO7pAACZBA5BI4cuSIcn+HHpQoUUIGDx4c9p2BddS0adMErvuojAr7yxXVDbx69apSQq1evdrq58PtB8gzj78luXPks9LCbcNURl24cEFZc5mKtHBrL9sTmwRMJRQUQto9ayhpaGVU7ty5lWWUbpO3Nnz++efq/2nRokXyzjvvuLjm8VYumo+tX7/erXubNm0SWJNSSIAESIAESCBSCWzbtk2++OIL1XxYQ5lxIE1FlNOElAoVKigXrSiMyRoUEiABEiABZwJURDlzYSoJkAAJRByBl156SQ4dOiTp06cXxLOIFClXrpxg1nmmTJmUMmrIkCGR0nS2M4oIvPrqq7J8+XKrR4jB1KB2B2s/nDegjGrZ+HqsNQwGw6pry5Yt4dxkti2GCGiFDyyhkkoJpXFDGYW4amgLRLdNH7ev8d8Eax9M7qBcJ6AH6bAHl6BanBRU+hjXJEACJEACJBDuBL788kvVRCig7G75TEWUk0UUCmqrKLrnC/crzfaRAAkkJQEqopKSPs9NAiRAAgEigJlXehAdCilYGEWSlClTRkaOHKmajEFCDP5RSCBUBGD1gO+dFiihoNyJJGnV+HG5rUBJ1WRYR0IZtXv37kjqAtsahQS0oicclFAaL6yxoBDzRRlVsGBBNUlCl431NeIhffbZZwpDzpw5BS6AtZgKKp2m10ePHpVff/1VLagDgoDw69atkwULFsjOnTvl0qVLOru1Rpou9/vvv1vpThunT5+28uJ8gZZr166puCF41po3b574c46LFy+q32X0Gc9t+/fvF1jj+ipoAzjADfOcOXPku+++E7iUpZAACZAACSSegFYgQQmF/39TTEWUk0UU8mpFFP63du3aZRbnNgmQAAmQwL8EYjvSLr8GJEACJBAlBGbMmKF60q5dOxWIPRK7VbNmTXn++efltddes5RSTz31VCR2hW2OIAIYGBw1apTV4khUQqHxqVKmlFZxVlFDR3VXfUH8lj59+qjByrRp01r94wYJhIpAOCqhdN+1Mqp169Zy9uxZZRmFY61atdJZuHYgsHXrVjl37pw6ct9990nRokWVFTbSEPfr9ddfd4yhNWDAAGuyDBRWY8eOldmzZ7ucAYN++C0uWfK6Qh0HN2/e7PJMs2fPHvH0e/bKK68oBSPKQUEWSPfEUBh17dpVfvzxR1RvSdWqVaVfv37WvqcNKJsmTpyonm/seRAzc9y4cQK3Tt4Eyqv+/ftb/M28xYsXFzz/tW3b1kzmNgmQAAmQgI8EMIlr48aNKnfz5u5xYU1FlCeLqEKFCkn16tVVjChMNihWrJiPZ2c2EiABEogdArSIip1rzZ6SAAlEKQE86GJJGTcQ3b59+4ju5aOPPio1atRQfYCF1LJlyyK6P2x8+BPAwCcGVyGRqoTSlO8uW1Pq1Wqnd9VszPHjx1v73CCBUBEIZyWUZqCVUb5YRukysb7WbovAoUqVKpI8eXJrBjiUUbBsik+WLFnipoRCmX379ilXf7Bs0lK+fHmB5ZUWbfmt9/X6ypUrgnq16Fnpej8x659++knq16/vpoRCnZg9//HHH3utHlZMeLbBJBsnOX78uEAhCis9J0HfunXrJj179nRUQqEMuENJBWUchQRIgARIIOEEdIxYKJIqVqzoVoGpiPJkEYVCDz30kCqLd3MKCZAACZCAO4GYU0SdOXNGMPuZkjgCcKWB2YZY8AJFIQESSDoC2hqqQ4cOSRIAPtA9x+zidOnSqWonT54c6OpZHwlYBBDTBDPzIV3aPB9x7visjhgbcNGXM3s+K+Xdd9+lexCLBjdCQUAroXCugQMHhvX/EpVRCftGrFy50iqgLXhgFaRlzZo1etPjevjw4epYjx495MUXX7SCuyMRyizzfx+DfZ07d1b58aGfd6yEfze+//57S0mDOJm6bfZ8/uyPHj3aqhvl8f3+4IMPlGIJSrKpU6d6rRaDkStWrLDylCpVSl544QWlWEJbtQwaNEhMJZxOh6LLLI9zPv7444L8cB9l1gGrq4MHD+qiXJMACZAACfhIQLuXbdq0qWMJUxHlySIKBbUSa/v27XLs2DHHuphIAiRAArFMICYUUb/88ot06dJFmcYi2HCRIkWUAgX+tSn+ERgxYoQyOcbMkffff9+/SliKBEgg0QRwD2KQAwMRHTt2THR94VABBgYx0APB77SnWcLh0Fa2IbIJYIARUuKOCtK4Xme1HekfmTPdIk3qX5+Nib4gfgitoiL9qkZO+00l1BtvvBERru6ojPLt+4XYRDruHJQpN998syp4zz33WBWYiior0WEDA37PPfecsvRBvKWGDRtauTB4Z0qzZs2s3a+//lrFgbIS/t0wLbVgDQUL8UAIXPLNnz/fqgqxBGGZBFfCcIWHeFmmxZaV0diAu0ItUNrNmjVLHnnkEfWcs2jRIkuRZFfCoQwUU6+++qouLnCHiHNiwg4UdPgPW7hwocC9H54DoSDLkyePlZ8bJEACJEACvhHAbyf+z8z/HLOkqYjyZhGF/58GDRqoov7EEjTPyW0SIAESiEYCUa+IwgsRzGsRiBwP+FowePvggw8qhVR8wW91Ga7/IwA3EVrMmYs6jWsSIIHQENCDPrCGKlCgQGhOGoKzwK2BdtGHgZV//vknBGflKWKJAJ4LMKgJadeqb1R1veo9jSVrlv/cWSG2CBYKCQSTQCQqoTQPKqM0Cc9rHcQdOfBupSVXrlxWUPdt27bF6ykBcYzsQeAbNWqkq1Mu+qyduI3s2bMr13g6DYoru0A5o6VevXp6M9HrHTt2WHUULlxYateube1j48Ybb5THHnvMJc3cOXXqlItLv6efflpSp05tZcFzG57ftOj/JL0Pl3vm+yuUUjinKWCJ33c8D0JBRiEBEiABEkg4AcQyjM/Vqq7Vm0UU8iDm8cyZMwWT4CkkQAIkQAKuBKJaEQVLqIcffti1x7Y9KKQwuwwDUhTfCVy6dMnKjBeky5cvW/u+bPz1118yd+5cFZz30KFDvhRhHhIgARsBDJDAJQxmwkaLNZTZxbp166pduNyhwtskw+1AEND/+7VrtJCihUoGosqwqSNd2nRStWIT1R7EvYLAKgrWURQSCAaBSFZCaR5Oyii7YkDnjcU13pm0VKpUSW+qtakAgftub3LHHXe4HTYVU06/U1Beafnoo49c3jvgInzz5s36sIpdZe0kcuPw4cNWDXhfdBJ42vAk9necu+66yy2r6doQ766mmPtQ/uXIkcM8bG1jJj8UghQSIAESIIHgEPDVIgpnx2+ydtEXnNawVhIgARKIXAJRrYjScR/iuzxQpMB1HwK8mgqW+MrF8nG74imhcbcwexCzAocNGyZ4md2yZUss42TfScAvAohvA4ESKhoHIKCIypw5s+ojrKL+/PNPtc0PEggEAT2D/rHOQwNRXdjVcW+l6xYGw9/uKVUq1lFxohAvikICgSYQDUoozcSujOrWrZtgMkSsC96PFi9ebGGAyzgopvRizg7XcTaszLYN7dLPlux1t3LlypYLPCieTGWXfhZCBXDLZ7cY8lpxPAdNRdItt9zimFs/pzgdNMvny/df7D4zb9asWa1deOkwLcB//fVX6xhd7lkouEECJEACISdgKqLM/7yQN4QnJAESIIEIJxC1iqgjR44oH9z269O4cWPB4KbTywACvMLfN5VRdmru+6ZrPhzVL1qYxbhv3z755ptv1Mvpt99+K3ArYfePixdXU+CGi8EcTSLcJoH4CWj3NBigiUbB4I62isJvjBmsOxr7yz6FjgD+gzDT/Y7bo8sSyiR4a+7CUu1fZVTd6p3UITOOipmX2yTgL4FoUkJpBqYy6syZM/LAAw/EvDLKtDgCJ8Q46tSpk7WYcejgIs4+YU2z9XeNeBymCzszdqT5u3b//ff7ewrHclevXrXSr127Zm37umEOVnp6v7S/U5mDnalSpbJOhe8ihQRIgARIIGkImL/N3mJEJU3reFYSIAESiBwCUamIQiDdpk2bul2F4cOHy5gxY2TChAmyZs0amT59uptCauPGjcqnq1thJrgQsL80Iahj2bJl5fbbb1dxXVq3bq1eTlu2bKmCNd59992yYcMGqw4E1DUFsxthlQaXfRQSIIH4CaxatUr27NkjefPmlfLly8dfIEJzaEUUmg/FNoUEAkFg6dKlqpra1R8IRHVhW0e1e66751vz9UIZ/cY05b5q165dYdteNiyyCIwcOdKa9PXUU09Jq1atIqsDXlqrlVG5c+cWrYyKZUWAfQKZF3QqphFiRQVaWrRoYVUJ6yy8O0DhtWzZMiv93nvvtbYDsYHrr+XkyZN60+e1acUEaycnBZ05WQ8TJU3llRn7E++3FBIgARIggaQhYCqiUqZMmTSN4FlJgARIIAoIRJ0iCoNLCFKLh327pEmTxiUJVgTz58+XUqVKuaTPmjVLpbskxugOXELAbR5cGE2ZMkVeeOEFgVXZb7/95kIE7g3xQuhNYBmlBXXZrdJwPCEvurourkkg0AQQDBrfT8y+hfIaiutwEz2QbgYMD7c2BqI9NWrUEB1PAhMFKCQQCAKYsQ+pUTF6Bs6duNxVqprcWbScrPxipuze/aPKQstCJ1LOaYgP9NVXXzkfjPFUPCu/9dZbigImHcHdcrQJlFFQcuA/KNaVUcuXL7cu75NPPikI6m5fzIkjwXhuQnwk8xyLFi0SxMrEOwgErr69ucmzOpCAjZw5c1q5dVxBK+HfDW+T6ExFFrI7cVmyZIlVpal4QqK5/+OPPzKmsUWKGyRAAiQQWgKmIsqcMBDaVvBsJEACJBD5BFJEfhf+6wFmxyH2kJPUr19fnILMwk85gt7ixcZUXg0dOlTg3iEaZzvArzvc52HByxtekhA81/TZjhl7AwcOlKlTpzrhTHDaPffcoxSEuiDOhxc6vHzNmDFDtm/fbr1I6jxck0BSEUiXLp16+ceAgR40uO2229RvSK1atQQWfkkpuD91fIRq1aolZVNCcm78PmMm8MGDB5V7JAwOUkjAXwJQLmDiRI3qdfytIqLKVSxXT77/YZMc+v2gajd+06JRaRCMiwJFCxRRmJgAS4sqVaqo4NM33XRTME4XMXUiZtLgwYNVe7t27aqeFyOm8QlsKK413MDB0h/9hpu+jz/+WGLpO3DgwAH1zgB0WbJkkd69e4s5IKeR4tleK6ywDsbvTNu2ba1z4P2tUaPrsfDQhkC75UOd5mRFPIfgvcV+ntmzZyOroyBeFd6B8L8Dee2111Sd4AjB7wu+T1rwjGlKsWLFVGws/Y4KV5iYIBWtLpnNvnObBEiABMKJgPm/R9d84XRl2BYSIIFIIxA1iijMbnZSQtWsWVP69OkjxYsX93htMmbMKJMmTXJ5scAD/6effhoVbkbg3xwuMvBSiD7plxkTCF6IoBjKlCmTSoYFlL9KqKpVq0q5cuWkdOnSUrBgQcmVK5eLmwl9XswkgXUVFgoJhBMBDJ5gwcABgmBj+e677+Snn34SxEHAdxuKbSzaWieU7YeLOsTBg8TCYISpeMKgjbkfSu7Rci7M3ob7Wszwh0tUDKrdeuut0dK9ePuhBwRLF4/O2Gp2ACXuqKiSDh/+Wfo/M0KGvtFHxcfCfzPFOwFYxeIZCnEvYcmNBdb1mACAweWKFSvG3O+RVsbg9+ONN96Iiudk798CUUonuzJKWyXHVzYajq9du9bqBt6rzME460DcRokSJaxdKG3gci579uxWWiA2oAzGOwsmE8BCCO7WtdiVODo9MWsooZs3by5z585V1fTo0UMpJZGOmFF4/4zPDeFzzz1nuYxHm2vXrq2U2oira1qowvoKik5T0qZNK8OGDVPuzpGOfkMZp92h473t77//lj/++EN5q9CKLrMObpMACZAACSSegPnfR4uoxPNkDSRAArFLICoUUbDsefjhh92uIh7G27Vr55bulIAZZ/379xdYQmmBO46k9nePge8PPvhAzcLEy0fWrFlVHCbMAKxQoYLHl0H0AYHY58yZo8qjrDfBcVglaeuKhPpB79atm3rJQoyoYP4xw5oLTPDChcEh8MBLbqAGUfXLHAIKwx1GMPvi7XrwWHgQwEAjFiiz9+7dq6yQoKSFNdLWrVvVIBzumTp16qhYaIF2CeOJwqZNm9QhtA2zbaNd8PusBQPCDz30kN7l2g8Chw4dktSpUytlJv7zsOA/Bd9jLHY3tn6cIqyLwHoakjlTvrBuZ6Aaly9vYSmY707ZtPULqVujraoWg6edOnUK1Cmitp4GDRqo33ZYkS1cuFAWLFggFy5cUIPHegAZMfpgtYl7B4PT0SymEgru+JL6GTmUrO2WUbBMgSIuFgTPPVq8TX7BMzPuBW0VhWclM66TriMxa5wDEyg0e+2WD5ZLwVKuI/7Zl19+abkgh1LSFHjd8KaYLFOmjDz++OMyduxYVQzvXHANbwpi56JP+G+2C1wwY3IUYrJp2bx5s4r5p/f1+pdffnGx4tLpXJMACZAACQSOAC2iAseSNZEACcQegaiIETVkyBC3K4cBFl+VULowBhxMWbVqlZplZqZdvHhRzYiFb3xYSARLMIsQ8Zgwuw+WSXjhQFwmDEBjHzPmHnnkETUgYm8DYi2hL5ipi5ea+JRQujx8r2vxZHEAl2Q4N16YTMEgJqzOgqW4gQ94+KQvWbKkmpn46KOPCmYlYiAEFliIC4aZyidOnDCb5bYNBRMCtZ86dcrtGNhiQAmKBVi6oJ/e/L67VcCEqCZQpEgRgQuiadOmqZgRvXr1UkphDFC++OKL6juDeAmhiGMECy1I3rx5o5q57hyCfesBXsZr0VT8X+O7jNgaUEDpGewYZO/Zs6dgtvvLL78ctXFxMBsdSmVItkz51ToWPkoVr6q6mSP7dUUJJnRQfCeA54Lhw4cr64d+/fq5WEHBQvWVV15R9w6eS2B5jkkt0SawgMKkI6yhdBsxYkS0dTHe/mhlFFxa4z0AyqholytXrgjeh7Tg3cKbwIWlFvP/2knBovNhnRBX6E7KLbwPBEvw/AHlPb73puBdqEmTJvLmm2+ayY7b+N3AxEJ4irALFE2Y/AhrL08CZRgUfMhrfwczy+D+pJAACZAACQSeAC2iAs+UNZIACcQmgWRxbgWuRXLXMfMLD+WmwEoAg8X+KEWggMBAlRa4YoDPcy146cTLpxbED2jWrJne9biGddL06dOVGy+7wsteaPXq1T7PVEZgXvhI132F4gTKIj1D0F633kc5DO7ipRoL4t/Y2wX3Ra+//rrkz59fBQDGAKX2aY70d955R1dnBXO2EgK0AcUSBjsS4iYQLjSgnLS/qMGCCoo9WNBBMKMRVmUQWIOZfuZVYtwHZnaabj90OtckoAlAgYnvkunjH79J+D7B7WSqVKl01oCsYa0I14AQDEwEIwZDQBoa4EqgeNezrKFIiG9QK8Cnj+rqMMiIQTas//zzT6uv+J7hu4wFM7qjQaCAw8x0/GfOmvR9NHTJpz5s3bleBg3vIkP6T5MBQ9sp11DvvfeeT2WZyZkAYsXg+6Qt7MxcsMzAoDWeIfC8FemCwW1MzoFFFNzR4j8Pz46xKuCAmFFnz55VE75iyTIsXK45JpTBOku/72DCnn5HCWYbEaMTEwOTJ0+uJshgDTl27JiyJkaMUf1O5qkdcMmHuFvIB48O8eV3qgeeIXBOTLCDFTPuR3iI8Kcup/qZRgIkQAIk4Erg2WeflZkzZ6rEhHhecq2FeyRAAiRAAhHvms+cpYfLCf/aUJDE9yAO/RteBPDCYAoGDExFlOmiCNZQphIK5fAy4ovA7R8UTBDMqsULrJPA0iEh7nI2bNigYhdoVxnw465fyuz1Q0GHATjk9cWcGPnnzZtnr0btwyWeKXCZ54/gRRIuzqAQgh90Lbg+cFsBqzBP/dF57WsoD2FBBQsp000HBli1EgplvvjiC3VetB2D3E6CgW9cNwzEUkjAiQDioWGBe1C4bYJlCb4zWN5++20rDhqUvYEQuKbTAmVyrAgsd7QiCr/dVEQF7srreGcYVIWFn3b5gzV+nzHhAvEoYBWC38JIVkrpGIl5chcIHMAIqKnEnZXi/vdTyOKVH6rWag4R0PSwbSJiq2HBMyOUUVi0tR0mH02ePFktUOhiggwsNiLVlSqVUK5fQ3gNgDIOz/LaKorKKFdGwdyDMuj555+33g/atGkTEiUU+oT3y0KFCrl1L1u2bG5pnhLwvgPL5MQI3sPs72KJqY9lSYAESIAEvBOgRZR3PjxKAiRAAr4SuD6Ny9fcYZgPihhTBg8eHO/LCCxjYEWAWZ3wMw6rKi16ZpveN2d8OimdECQ2PkE8AQxKa3GaPYtjcO+CALR26dy5s4pJ88MPP8i4ceNUu808mMmuxVSc6TSs4VYCriMwkOiLEsos67SdIUMGl2TMykuogDuUYlC8wWWFKXBfAddnnpRQsM5C8F7MRsEAj936CQNDDRs2VJZOul57/B4owf755x9lVeJtUM7ux13XxzUJmAQwqIABKdyPsOLDgD2+46NGjVIz42G99Pnnn5tF/NqGAlVLrLjmQ39N5QcUUZTAE8DvOixj4WISkxDwv/XSSy8p96dQSkEh1bRpU2UFjFgVW7ZsCXwjglwj4mNBcueMLUXUDcmTSeniVeTrTStU/73956kM/PCZQOHChdVzBH77MRHKbl0OZS7uIyitxowZI5HGHv9rsADC7wOeI83nYp8hRWFGKKO0e0IwghcBSnAJwOUlnsmheDLfpeC6m0ICJEACJEACwSRgjhMGYjwtmG1l3SRAAiQQzgQi3iLq119/deELy4T4BFZU2tIHA8NQouDFBgoq+4ukOXsVLrHsgthE8YldWebkvxsDq4h7ZBe4zqldu7aVjIEMWHGZVlOw/tEC/+JmoGCdDiUarL2g1Ordu7dkzJhRH/Jrbbckg7ItoQK3e1rRBEuSV1991Rrg+Pnnnz1WB2Wj2X/EAsP5MTsVA6U6JhbWcI8GBR9mKtqt5PBCCwux+JQDCEgOhVVC/Nd7bDwPRD0BuOLDzHcsiCOnraTwm4MFg1cIbI3fDn9mxO7Zs8diGEsWUdodITrvz++NBY0bPhMoUKCAPPTQQ2qBpQeUoPi9xP+kVkxFmqUULFUg2W+JLUUU+lzktjLy3bbV2FT/k3DpFGjXoaryGP6AEgoLYnAh5gsW/YyG51XE7Xz//ffV/wMsaPz5Dwgl3kmTJilPAFBC4RnLU/zQULYpnM6F520oGfFcivhZcNFLRoG9QpjQg3tm//79anKEvXZ4qjC9H9iPc58ESIAESIAEAkGAFlGBoMg6SIAESCDOw0CkQ7C7hPNlprxpAYX+79y5U1nXQDmze/duC0nx4sXFtKKxB/dGLKYcOXJY+T1twN2eKSVLljR31TZe9rUCRR+EH9ry5cvrXbVet26dUp6YiWinFihbxo4dKy+//LJjXCW4ipk9e7Z0795dOnbs6LdCCkq7QAtmquuZtp5cXCAeFqy67AJLMPQHQYM7dOgg27Zts7KAx6BBg5QrRisxbsPJ0gkKLlhiQYmg3fhBWYbviGmRYdYTKdtwrbNx40bVXP0ghZk9enYP0sx9ZMQ+0tUxrOP2ITpNzwbSZc1jZlmkm/s6P9a6jP24rtvTceTHohWMyI99rFGv3tf57On2/YTk03X7UgfcTuKeh1IKC2aWY8FMaihXYK04evRoxcGXD/N3IpYUUabVY3y/8w8++KByWYprqr8L5vfkBqTHLfr7Yl57va2P6fK41vp624/pMt6+F/qYmVfXrevzdMw8rtuA/qC8WcZp2yyrt+3n1fu6brOtehuKeAy6YjIEJgogCD0mWWg3fpgEgCD2GFxHHvu5fPluhyKPVkT9c/mfUJwurM6RJlUal/bAMge/P4EU3Hv4bsQn+M/WC9xsYoG7KqRBOWYuOIZ9vcY2vrOJETzD6OcYrOEO2J5mpuNc+rjOi7W5rY+befEbjfYePXpULZjUgkk4iD2JiUZ41sECRQ/Kw4UrrDs8PQMlps8JLQuFMxQskIEDB1LB4gEglPVghUlLffr0kaVLl3rIyWR/CCAWElxtO8mcOXOUa2SnY0wjARIgARIggUASMJ898X5EIQESIAES8I9AxCuiMGPbHJhF8NfcuXN7pYEBMrtMnDjRniRdu3Z1Sbty5YrLPlz7xSdQbNkDgmOgxhQMUCBulF1ef/11wYIZ51CIwarJjF+l88MKyhQMGMK6qHHjxvLcc89ZChWdB4oVzMrFggGPznFWUgkN8Av/7KbY2ZjHPG3braqgiNJMcV3tAo5OSigzH5SJeGGF6z79vUBcGSii4IrPm0CBBQUevh9w2Qj3VFr8cT2oy4bLGjG/7BZ//ly3cOlPNLQDLpuwQKGNeAe+iGmZiQFNU1nuS/loyANrRm+C3xEM6mLQF99x/F7pbT1YrNd6IBl5kKYGl+PWV+IWvW/mRX3mALS3dsTaMSggsMDq1i5Dhw51dD1rzxfsfa2IOnvO3cI52OdO6vpTpfovDiPaEgxFFP67d+3aJU6W32b/YdUY65aN+F05cuSIWjQb3D+YNBIOiig8N0HgVpbxj/QVcl5jYgmsnDHJBG5L4f6bEhgCpmcK1IjJd5jsgAlj2bNnD8xJWAsJkAAJkAAJxEPAVETpCY7xFOFhEiABEiABBwIRr4hCwNhNmzZZXXv33XcFA+7epGjRot4Oq2OYfY8XHVMw+GgKZrl6EwwyvPjii25Zbr31Vpc0zIz1Jphx7kkwQ9103WfmgyUGlDBwpwKLK23hY+aBJQaWHj16KMWbr4MfGNQ1xb6vj128eFG1AUqgFi1auATqtg+gY5b9fffdp4u6rEuVKuWxny4Z43YQtwuzvLUiCoNtsKDwpohCPCnM/NVKynvvvdelWl2XS2KE7WBgBErI7du3WwPzGHyHeyY9UI+1mab3I6yrEdNcfN9uvvlmZWniS6NxP2l3lsgfq4oo+8CUyQ6Kflj+4QUBSnmszW2kwbLCnoZ0zG4zF+TBNcJap2M7WIJBfPynYWIAflOx4H8Eyi+tDPO01goynV8r2JzWOg15sa0XXbfe1/kwsJoYAdtwEa3EPHfO+8SEcGlvINuR2qaIyp8/fyCrV3VB4QjrHvzP4N7BfYO1trqzb+M4jum85ravefHdx2+jXqDg0ttY6/84rPV9pdMux91jl/691/T9FnAoCagQE4RKlCiRgBLByYp7HkoVKPWpVImfMaz5wQmxouB1gMziZ+ZrjoIFCyoXx1A63XLLLer/2NeyzEcCJEACJEACwSCA51cKCZAACZCAfwSCN6LmX3sSXAoz4+CTXQviVyAmC6yBPEnlypWlevXqsjouELsnQbwmuIkxBS9Apmg3Z2aauT116lQXJZk+Zg7KYaB/5syZ+pBa9+3bV/lDj0/5gT7873//cylr34GyrH379oI4SrCGmTFjhqNLunHjxgmWhx9+WM1m9zbQi3PYlUh2F4m6HfDrPmzYMLWLPD179tSH3OpA+5ziZKFAQgZmMABmKu9y5syprqUnnlBaoY0Y9NKCNCgj9aC/DnCvj0fq2rTy8rUPGIzWg3daMaX3sfYlDXl0Xl02vjQc14OFuqxO0wPzWEOQrgfg9YC8XiMdx/WgullWD7zrsnpf1+srI3/zQXFep04dn4tD8WSKfd88Fs3bThaTur/FihWzrKD09wdrXFOsoZTG9wGL/i7iulOCRwCc27ZtG7wTJKBmDGrCYudsDCqi0qT575kmVeo0PrkWTgBaK6uv1p1WgTDawO+CVl7h90IvSMP3WP+WeGsy8uF3BguUYlhDAYrfa0y4+TUuVhTi3eBcWqDIyJo1q9sEKH081Gs8u0FefvllteZH/ARgNQYrsoMHD6rn7fgmxcVfI3OAAN7FMBmNQgIkQAIkQAJJSYAWUUlJn+cmARKIJgIRr4hq3ry5cl+nFQa4OFB2LFq0SF544QXH+AeYwYB4Qt4UUXDTZhd7MFzEIVq8eLEKTG3Pi3RPg/6wzNGWRwhibbYdFk5PPPGECg6vLZngks+UqlWrSqNGjZSrFFN5Yuaxb+OPE/E7sCD21Pjx4x19rsP9HZQ4GISApYYn0e3Xxz1ZG5m+8hHfylREQSFnKnugRMTAjJPFwV9//aVP5XUNd0BwS2iKjskFP/NOAgUcZnDbBUpO7couPqWjvWw07eO7A4VmfBaA0dRn9EUrpbSSSiu2tCJLp5v54IoT33PcQzpAveaC34+77rpLxTPALHPcXwkNVG+65UO9saiIyps3r4ojo7na155+d+35zH387mDRiimssY9BZ31Mb+MYvgPm90F/J3S6/m7odL2vvyuoU6fpPFiHq+A/E4u2DDPX+A/as2ePsp5ALD3z/wzXCosnq92k6C8szmJVEZUq5X+KqFvz5E8K/GF/Tny3scQ3GcfXjnz55ZcqlhriqWGSjCmIH4V7AwsmSIWLwBIKz1Jwc01lSsKuCn7vwI9CAiRAAiRAAiQQXQRMRRQtoqLr2rI3JEACoSUQ8YooKDK6d++u4h2Z6OCSDgushsqXL68GfGGRA3/8UECZ7vzMcthGGSclDKxy4DLJtKx57LHHVNDpSpUqKWUGZru+/fbbMnfuXHu11j4GqqFAg2DmpCk6BhJmAHbq1EktaDfc6mFgBG79EMzbF8GgOAbd7AoEDC4MGTJEueODRcbkyZNdqsMgOtoHSy1P/tftiijM8LULBjKgrNNityhDOl7a0U4tULqhzVACmTJv3jxl5YbYT54EXOEWBa74tOD7oWf0YgayXRDLBBYUTgIXjqYiCgPRdpZO5ZgWHQQwwO6Lohf38KpVq1Sg8vXr17t0Hq6v8HtSo0YNtXY56McOFBimxJIiCsoDCAZvAy168NnX39ZAnz8S68Nv42effSYrV66UX375xeoC/iMbNGiglnAcxIZFFORMDMaISh1nBaUl36359CbXASQAhTIm4GBiDZRP5vMIToNJL1r5hLVpIR/AZiSqKlhnQWAJjv+3YPzmJqqBYVoYCigsVOCF6QVis0iABEiABEggEQRMRRTeHSkkQAIkQAL+EYiKX9CuXbsqxY9TDCQonbAkRJAfChG4ZzMFfziYbY/AzaY88sgj5m6823CnB4smDEDYXdrZZ8yisowZM0qZMmXirdfMAAWQdk/YpUsXgbs/+wxfWGjAjQjc4Y0ZM0amT59uVQGWiJk0duxYK83csCuVMCiJGf564B7bOtC1LmePu4R0KLpMRZQelMFgIQbwzWuHfsB1YNmyZZWyCuWhWIRSEe4Y7ZZjOI5+aUu2Y8eOIckSuOzzdu3KlSsnH374oZUfFi6waKGQACz0oOjGIDwWWMlogcIaimks+A4HUuzx7WJRERVInqwrYQQOHz6srI1h8bt161aXwlWqVJF69eopBZTddatLxiTewUQHSCzGiDr6xwGLfsGCBaxtbiSeAKwBoYBasmSJYzzOWrVqqRiYUD7BBV84CxRPUCLjua5bt27K/bVWToVzu5OybZh4BVaQhx56KCmbwnOTAAmQAAmQAAkEgYCpiKJFVBAAs0oSIIGYIRAViigoWGDVA4WKqdRIzFWEEmbSpEluVTRt2lTNEEVAaV/k7rvvVhZSWGvBDFlYTmCQ2m5ZhHhX/fv3d7TI0uV9WWNQRMsHH3ygZuc+99xzKh6NfQYHFDUIMN6iRQu16HJwbwgOmOFuF9RhutWDOyYo6Vq2bKliH8AizFQioY6GDRvaq1EWUWZipkyZrN3HH3/cpQ4cgOtAXwRtQ79N7nblGeJY2OOAmXXblU4YZLKnmfm5Hd0EoHxas2aNupdgAaWVQPgOwZIRiie4V4LbvWBJ6tSpVf36d+6HH34I1qnCrl7dV1iFUkJLYO3atUoBhf8E0/Xe7bffrv5TEOcsIXH8Qtt617NpRRRSN29fJ2VLVnHNEMV7Loqo26iISuylPnv2rKV8+uKLL9yqw/OCtn667bbb3I6Hc8LAgQOlfv36ysIHz4G+PvOGc5+C2TYooWA9hv9/KqKCSZp1kwAJkAAJkEDSEDAVUfbxtKRpEc9KAiRAApFJICoUUUAP6yUMkkEhBbdzvgosbNq1a6cGl81yGGieMGGCm8UM/oAQ5yi+l00oQpAP9eOPCi/xcBunBS5PIHYXdEiDuz9Y4iTGDdy1a9dQlSWwFurRo4fACggKISjAoPRBf+ByDgPb6LNd4IbQSRGFfJgxa5aZOnWqYHGSfv36Sbp06dwOwbrpo48+stLNWbdQIsGqCgMivgq4YxAA19TuVhDXA4PZUCgh8LGTYsw8D9yrPPDAA2o2MNLtbhTNvNyOXgKwhIO1IGa661ho+J41adJE4CoSliB2JWcwadx5552Wwt10fRnMc4ZD3VoRhf5Tgk8Av/34T8VixsjD77h2vQeXk5EmsLaFxRYUyd9tXx1Tiqhjfxy0LleBAq4W39YBbsRL4JtvvlHPEXiWgFW2KYjDiTieWHR8SvN4pGzjd/all15Sk5FmzZqlmk1llPPVw7O9duP85ptvOmcK81R4QcAkDw6shfmFYvNIgARIgASSjICpiKJFVJJdBp6YBEggCghEjSIK1wIvUFD8NGvWTA0cI2aQGTwdyiooJ+DeCjO5MZimY0FhcArKmilTpliX9dVXXxXEeMFsb7vcd999yv//xIkTlRILZTE4DfdzGJy7//77XVzhtWrVSqBkgUUP4kjpwNSwRurVq5eMGjXKOgXiCjz99NMybNgwFU/AOuBhA5YaqPPPP/9UyisMjKN+tMecvY7isMZCm30VuzLHLIfYXKYiyjxmbnfo0EEefPBBM8nahiunt956S7F3Ugx17txZWY5BEQBrFG0JYlXw7waUYm3atFEzeGE14iT4fsBVH6zRMFik3Qg65dVpsJpCHzEo27p1a53MdQwQ0Aoo7bISD5z4LYCLJSxJ5V7JnFmP352jR4+6KV2j7fLs3btXYJUDoSIquFcXLvfmz5+vFjMeIpT3WgEVyTFjYMFYt25dmTFjhmzZvia4MMOs9mP/uubLlDGz+g8Ms+aFdXOgcFq2bJmakABFlCn4P8CzH5RPOgaZeTxStzGpBzGPZs+eLVRGOV/FkSNHurCJtP+n8+fPq3cmPFtj0hmus2k16tzryEr98ccf1aQyp1Zjch5czVJIgARIgARIID4CpiKKEzfio8XjJEACJOCZQLI4yxlX0xnPeaP+yOXLl5U1jelSDp3GjPBguh3CiyCUV/ag1ngpfOGFF9TgNwJcm/Lrr78qxQyCxdvbi8ESWGxhgHrcuHHKRZ1Z1tdtxLKCRZA3wUCFJ2UUBi579+6tFEne6gB3KAwLFCig4mF5yws//IhTgoDgEAwq5siRw9Hayls9CTn2999/q3heOn5VQsoyb2QSwOASFKQQKK1h/YRBeHvcuKTo3YoVK6xYFDg/XIhCMR7N8s4778jrr7+uurhr1y6lZI/m/iZF36B8WrhwoYp5ps+PyRNQ2uC7H4nWT7of9jXcqGGSA2T44E/ltnyxYWXXtVcVOXnqmDRp/ICMHvM/1X9+eCeA7wqsYWH9BFd8kDRp0qjJCLCIhfLJ24Qd77VHxtE+ffooZRRai0lVtIy6ft2gtNGeDmA9Foku+ebNm+cS9xYuBl988cXI+GL62MrvvvtOmjdv7pgb71mbN292PMZEVwJ417x48aKaxIfnAQ7CuvLhHgmQQC0Zyg4AAEAASURBVPQTeOWVV6wJ3XgujLTJJ9F/hdhDEiCBSCEQVRZRiYWOh+q3335bzQ7EDDotsOZB7CYnN3o6T2LWUKaMHz9eunbtqixvdF2YjQ5FDgQD4FDUQBED6wC7pZMugzUUJxAMjrz88ssqdhbqh9tCXwSDK3i5LlasWLzZ4YYEL60LFixQefFSB16wSitcuHC85ZEB3EuXLu1TXgyMmu77fCqUyExOLgUTWSWLhzkB3P+4DxATDkqocBLMvIelJpTREFhuRbsiSiu7oWCHpSclMATg2hGzwZcvX259n/B7rK3+8L3Cb3q0CQbR8F2CFcD2nRtiQhG1ded6pYTCtWzcuH60XdKg9UcrLDPffLOKoYl7A/E9EZs0VmTEiBHKFTOeC6F8OXDggBqICfWzWDjxNpVQiI0aiUoo8LTHqbXvhxNzf9sCzxemIgrvUfqZwt86Y7Ec4gzrCZPg5+s7XiyyYp9JgASik4BpEcUJytF5jdkrEiCB0BCgIsrGGZZHcM8HN3HaLRGUPrAMmjlzZtAso6CIwYDgo48+Kk5xX+CCC0t8gpnrsOAwBa4nEGsJ1lX79+8X+ILHgn6hv4gVhQVxM6B8SojiJWPGjMrdHQYqED8nGl9iTZbcjg0CsMAJV4HrycaNG8vo0aNVE6GIimbBbGYsECgPKIEh8MgjjygFlK6tWrVqlgIqb968Ojlq13ALqxRRu9ZJ8wYPR20/dce+2PCp2syeNZfcVzvyYnvpfoR6/dRTTwlcUcISKJZFu5eGdRTiIen4mbGojDKVUJhAlpA4puH2HYKbasRPxWQyuLiORhfUcJcJK3ctcGVORZSmwTUJkAAJkICvBExFFK1CfaXGfCRAAiTgToCKKHcmgrhN06ZNE8xy1JZHWGM2WDB9iUNhBMurDz74QMWS0oowhya6JKEcZnhjli5i2Jh/kmbGVKlSCeLLmDFmzOOJ2UbdVEIlhiDLkoDvBKCIGjt2rHJRGe2KKHPAqFy5cr5DYk6vBODiFL/Zbdu2VbH17BMYvBaOgoNwOYjByW27NsgPP2+TooVKRUGvnLvw8/49smb9fHWwWrVazpmY6kgA8Top1wngnoGSGpb6iB0Vi8ooUwkFF4WRrqBErFRYumkXg/yukwAJkAAJkAAJOBMwx9gQO5pCAiRAAiTgHwEqojxww8z7DRs2KGufiRMnesgV+GS46XvssceUmw/MUEQbMHsPC5RhcEuFAUPM8EMMpkqVKkVVcOzAE2WNJBB9BOASpVGjRvLpp5/KsWPHZM6cOcptVPT1VKyZy/jdiy9mXTT2P1h9QuzDWBb8x9euXScuJtYKmb/0Pen3xJioxbF63Tyrb/fff5+1zQ0SSCgBxEP45JNPBJZRiFdYuXJlNYEqFuIkRIMS6urVq8ozgqfrDpfeeA9xkoMHDwpiupoCt+EYmDt9+rTy5oDYtLAgxP81XOJpQTmUNwUT2DDxD4JjsFDFew5cD+P3GbHYPAnqg3tiuFFGn/BOBPfl3sp4qiuh6QitjIkcOPfJkydVewsVKuTRbThcWeq4trlz51YxZ53OCYaoD2Kywf5ff/0lW7duFQx8YhY+LONxrXyZAGjWmzVrVuVWFPwQbxPvllAuFy1a1K399mumY+ShPYcOHXLsB645PGVQSIAESCAaCZiKKFpEReMVZp9IgARCRSBZ3AP1tVCdLFLPc+LECeXKDi9GsRQXIFKvF9tNArFA4PPPP5cuXbqorkIhPWPGjKjrNqyhdOwNuD+CGyQKCQSKwJYtW+IUuC3jBgkvS+/H3pKqFe4PVNVhU8/xE8fkqQEN4wZ4T8n9dVrIuIlvhk3b2JDIJgBl1OzZs9UANqz5o1kZFQ1KKHzbvvnmG6/u9+ANokqVKm5fTCgzSpYs6Za+ceNGpdCAez/tQUJnQmza+vWvx6Mzn1f0cSix1qxZo9wMw723KVAsvfvuu1KkSBEzWfDKimed/v37u6TrnWHDhqk4teZgoT6m11C+IAYoBDEQN2/erA/Fu/7222+VK0MdK8ksgOexZ5991kWRB2UOJg1qNohDjElETvLkk0/K/PnXLVcR08p0J6jdYdrLwSMGPGE88cQTHpVScDc/YMAAVXT48OFy6dIlGTp0qNUmHMAkx1deeUXF99XnWLJkifTo0UPv+rRGzOBu3br5lJeZSIAESCDSCLz++uui3ffj/w8TAigkQAIkQAIJJ5A84UVirwRiJ8ElFJVQsXft2WMSCFcCGEjRL/ywnPzss8/Ctal+t+ujjz5SZTHTmdZQfmNkQQ8EypQpI4883F0dnb90kly+csVDzshNnj53pFJC3ZgugzzW83pfI7c3bHk4EYDyAC6sz5w5o36f4a4vGmXw4MGW67pId8fn79xDWB05yc6dO5ViRitazDzdu3e3LHy0RZB5HHFvoWCxK6GQB3FsndwF9urVy6MSCuXgQj1YMbumT5+uvu9OSiicG27VmzVrJv/88w92lWDGPJRKWjy5d79w4YKlhELeJk2a6CJqDct3J0FbPvzwQylfvrxs377dKYtLGrjalVDIgOuHeHhQVGpxumb6GNckQAIkEIsEzEkOtIiKxW8A+0wCJBAoAlREBYok6yEBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEnAhwBhRLji4QwIkQAKRQwCukb777jvlWgZxomrVqhU5jY+npYhhtHr1apUL1lC0SI0HGA/7ReDJpx6Psyb8Qvb+tCMuVtT70qJh9LgVmj53lHy+do7i0qHNo1KipKubK7+AsRAJGARgzYJYM4jvg9/paHPRB6scuOWDwDVsq1atjN5H3ibc4cEFmymIgwvrJG+SLl06gVs3yPLly63YjWADa5q6devK/fffL7CQMuPq4n+8Q4cOKuaTLg/3dPp8kydPVnXCuhuu/5YtWybaamjbtm2yZ88eFW8KmfA8oF3XYb9q1arK9R/iJqFNcP8HgYVQmzZt1DlVQgA+jh8/7mKJBdeBbdu2Va79ELsJ54TgPoDrwI4dO1pnbdq0qcA9HmTp0qUq3pP9eQaWYVrgJs/uHhExqOCOEG71Ll68KGgPYlRt2rRJF1Pu+VC/vW4rQ9zGuHHj1C6+x8WLF1dxiMFOCyz+9Pe9RIkS1jXH8UGDBlnu/Hr37i1wC2gXlKGQAAmQQLQSMC2i8N9DIQESIAES8I8AFVH+cWMpEiABEkhyAggq/vTTT6uBHgzeINZB2bJlk7xdgWjApEmTVDUIZt66detAVMk6SMCNAAZYe3TvIU8/84RMnzNC8uQqJBXKXo8f4pY5ghK+3rxKZs0fq1pcqEBx6dX7kQhqPZsaSQQ++eQT9RuNQXjEBIp093WavamEggvCYLl80+cLxRrKAyiGTFmwYIGlGDLTze3UqVNb/8NHjhyxFFFQfMAd3ZtvvinJkycXKF3grkgrPA4cOKCqyZMnj1Uek2a0IgrlEdcJiiNI48aNpUGDBkqhhf1Dhw5Ziijk04LYRYjHpAcFH3zwQaVg00qwCRMmuMRY0uX8Xev+oPzdd9+tFEt4/oLA9R4UR9pV8ltvveWiiIILWHDXLv2+/PJLpbRThf/9gAJOS4sWLRRDvY91sWLF1GKmYRvxrqBUgmIKTBFzS8flsufV+4g9pd0Fdu7cWaZOnSovvPCCOoyYJ3DDiGuZP39+tehyuMbaBSOUjoULF9aHuCYBEiCBmCCg/3PQWbrmi4lLzk6SAAkEiQBd8wUJLKslARIggVAQqFatmiDINUTPLg7FeYN5DszI1QHEoYRCQHEKCfyfvfsAc6pYGzj+0ov0KkXpXaSIiggC0jsiTT4pShHpCNgo6lVERa8UwQZKu1yKSC8CIiKwdLBQpNcFVEClSv14h3sOSTa7m2V3k5PkP88TTp/zzu+EXcibmUksgWYtGkm9Wk+YD+BGfPKC7Pj19rfME+ueiVnvtRsiw0f3sG/xbIfnJG26VPY2KwgkpECGDBlEk1ElSpQw1bomcBLyPv6sy7UNmoTyNo+RP+Nx8r10biFNXFilWLFi1qpo0iqmogkaz/kfNeliFWtupL/++sv0NtL9+u8BvafrB4K6X+ePsoomRROyRERE2NXpnGFWEsraWbt2bTOXsG5rUkjjtYraWIk23Wf1+LKOX716VebPn29tRpkfyj7gZUV7Sr344ov2kd27d9vr3la0R5wmC12L9mZzLX/88YfrJusIIIAAAl4E6BHlBYVdCCCAgI8C9IjyEYrTEEAAAacK6BB9mrjRYWuyZ88ugwcPdmqoPsU1duxYc55O2K09vigIJLZA/5d7yJp138nfZ/+Udz7qIWPeWSbp70qf2LdNlPp7vFTLJNW08rZP9ZCnOzROlPtQKQKWgJWM0i8OaBJAEzlagnEoO5JQ1lONfanDu2nPGdfSsGFDqVatmtmVMmVK10NR1jUp4prE0hNef/11exg8K+ETGRlpX6uJrjNnztjbriuaaNGeQQmdiNIhB62SKVMmu3eTtU+X6mANlXf06FHJmDGjfbhRo0am15ju0OEKdZhC7Y2rRa+xehppYk57UHkrmrBav369aC8z7V2l1+TMmVNOnjxpn37gwAF73dtK8eLFo3jrvxl1OEArBh3+j4IAAgggEFXA9QsQ9IiK6sMeBBBAwFcBElG+SnEeAggg4GCBQYMGmaFvxo0bJ/otWZ2/IBiLDoGzf/9+KVmypOg3jykI+EOgcOHCMvTtN6Vnz55y9u/T0m9wQ/ns39/749YJeo+X3mwlJ04eMnU+0bCjDBxyKyGQoDehMgS8CGgySn//1K1bV86ePRuUySiSUF4ebAy7PJNQemqKFCkkc+bMMVx1+9A999xze+N/a5qgsZI01kFriD/dXrt2rVSsWNE6FO3ywoULUeqJ9uQYDpw+fdrtqC/31l5RrkXnlCpTpozovFdafvjhBzOvlq4vW7ZMF6Zo7zvPxJwe0C8Zaa88a0jDW2dH/fPatWtRd7rsyZEjh8sWqwgggAACcRFwTUTRIyoucpyLAAIIuAvcHkvBfT9bCCCAAAJBJKDDIulk0lp0UmvP4V+CoSk///yzmS9CY+3Ro4foN48pCPhLQOcn0flttPx+6ri8MLiJv24d7/v8c/Nb7H0GNpTde7eaumo81kIGDxooadImiXfdVICArwI6F5AO05c+/a3ehK6JHV/rCMR5f//9t5lbR4eF1cJwfL49Be1NE5/i67C7qVOnjvNtYuuN5WuFOj9WXIu3eF17B1r/Prtx44bMnj3brl57k3kWTULp8MueSSjtxURBAAEEEPCfgJWI0i8MePvSgP8i4U4IIIBAcAvQIyq4nx/RI4AAAraATkCtQ9LoRN3dunWTadOmySOPPGIfd/KKzqlgfQijk37rhOUUBPwtoB8WXrp0WQYNelUOHN4p3V+uKx++OV9S3vyWv1PLid+OyevvtZWTvx81IRYv8oAMe/sdyZormVNDJq4QFtDerJqM0mH6gqFnlCahdJ6iHTt2mKdCEsr3N+ddd93l+8lezvTs+eTlFLPLs+fV5MmTozvV7NcPCKMbNsn6IFFP/Oeff2KsRw9qGzVhZvVyGjFiRKzzVuoQeJ6lfv36N3+vDDK7Nbn07rvvyp49e+x6ixQpIp7XXb9+3W1+svbt28tzzz0nuXPnNnNk6XB9ixYtMj15Pe+XmNsM35eYutSNAAJOFbCST9bSqXESFwIIIOB0ARJRTn9CxIcAAgjEQWDgwIGya9cuWbVqlbRu3Vpeeuklk5SKQxV+P1XnTOjevbu5r86lYH1Y4/dAuCECNwXatv0/OX/2kgx7918SeXy/tOpUSoa/8bUUzn+f43x27d0mb4/saoYT1ODSZ8gi06bOlMw5SEI57mGFUUDBkowiCRUcb0pNvLgW7e3kyxB5rtdY665zN+m8SJcuXRJvPZis83VZunRpWblypdl1+PBheeKJJ8x6XP7QZFbt2rVl6dKl5rLVq1eL9gK3imuPKWufDgto9YTS6z2HK9Zkm+scUdZ1ibHMkyePPTfWwYMHpVSpUolxG+pEAAEEHCtgDccX3RcdHBs4gSGAAAIOE2BoPoc9EMJBAAEE4iug3xbWYfr0H8r6rVsd1uWPP/6Ib7WJcv2QIUPsJJQOxff2228nyn2oFIG4CHTt1lEG9H/JvmTAa83k2x9uD6FkHwjgyrrNy+S1d9vZSagsmXNIxA+bSUIF8Jlw69sCVjLKdZg+HarPKYUklFOeROxx6L9lXBM1HTt2FE3k3EnxnL9q3rx5UarRnkiuxeqtrfv+/e9/y4QJE+TKlSuup/i07prA0p5M8+fPt6/z1gvctfeW9siKjIy0z9cVTVKNHz/ebV9ibeg8V1YZPXq06N8fCgIIIBBOAlYCylqGU9tpKwIIIJCQAklujk99IyErpC4EEEAAAWcITJ8+Xd4ZNkxOnzkjRYsWlcGDB8tjjz3miOB0Um3tBbV48WI7nuXLl4sOT0NBwCkCn3/+uQx/9z3558plE1Ljeh3lmda3E1SBinPanI9k+uxR9u0ffrCqTJ4yUVKlZk4oG4UVRwjokHfWMH0akCYUrLnYAhVgOCahdBi64cOHRyH/+uuv7eHhqlSp4jY8XM2aNU3PI/09vXnzZnOtLrds2WLWNTlRo0YNs65zKQ0YMCBK/brj448/tu8R3f2KFSvmlmzyrEh7B2l82ovJKvfdd5888MADokP8pbg5fOqJEydk79698uWXX8Y4x6S+Bzds2GBVY+q9++675czNfytt3brVDNWovcmtov9VbtOmjaxdu9baZYbnUx/9Ao0O36eJol9//dUMk1e5cmX7PNeVixcvuvlaxypUqCCzZs2yNt2W5cuXt+20vZqw0rnYfvnlF/nqq6/MMZ0vSl10WalSJbnnnnvk1VdfNV9GmjRpkvm3n1batm1beeutt9zq1w3t3WS5rlmzxtTveZIm7Hr27Gnv1ntpDy9106H61D5Xrlz0aLeFWEEAgVAT0KHvhw4dKhkyZHDr0Rpq7aQ9CCCAQGILkIhKbGHqRwABBAIooEP0aa+jAwcOmCj0G7n6qlq1asCi0uFoNCbrwywNZOTIkdK0adOAxcSNEYhOYNOmTfL20Pdk85b15pSihcpJ47rPyKMP1Y3ukkTbf+K3ozJx2juybvOt4Z30Rp2e7SaDX7v9oWmi3ZyKEbhDAaclo7p06SLffPONaU24zAmliYa4DqfWo0cPk1zS39cTJ06M9elbw8h5nli9enXZv3+/52637Xr16sknn3zits9zQ5Mkmgyx5mvyPG5ta0+j+++/39qMstRh9nS+peiK9oAaM2aM2+GjR4+aYY5//PFHt/2eG/qFn06dOnnutrdfeeUVmTp1qr2tK8NufmFIE13eyn//+195+eWXvR0y+3r37i0RERFuiTU9oEkxHXIwoRJR3pJxnkFpYvK7777z3M02AgggEBICX3zxhRlxJEvmzLJ127aQaBONQAABBAIhwNB8gVDnnggggICfBLQHlH7o8cwzz5g7zp49W9q1a2c+9Jg5c6boZNf+KvoNZB2aST/kcU1C6bfjSUL56ylwn7gK6LfVZ8ycKl26dDWX7t63Vd4f00veHd1Djh67leCNa513cv66LcvljfefsZNQd+fMK2PHjiUJdSeYXONXAc9h+vR3T6CG6dP7+pqE0qSBfnFj+/btfvVKjJtZc1vcSd06J1NsRXvIRFe0t1RsxZd7PProo/LDDz+YhJD2vomuxDYUcbVq1USHl4suZv22u2fRXkj67yftVRZTz+3Y7t2kSRPPqqVOnTpR9lk7nnrqKXNPnSPKtWgMXbt2NT3LtUdYdMWXIaR8eT46TKAOA6jJSc9YrHvHlmy0zmOJAAIIBKOA9fM0aTLmYQ3G50fMCCDgHAF6RDnnWRAJAgggkKgC+q3ZTz/91O0bq/phhn7QpsPrFC9ePFHu//3338uMGTNkwYIFbvXrh5M69Jl+wENBIBgElixZIm8Pe1cOHbz97f4nGnaRdi0Sd+6baXNG3xyKb7RN9Hj1OvLa669K/vz57X2sIOB0Ae0Zpb1Fjh07ZkL19zB9moTSJJgWX3pCffjhhzJixAgz/Jj+/sqePbu5lj+cIXDp0iU5cuSI6JB3SZMmlYwZM5rh4awPC2OLUnv5HD9+3MyhqUP7aWJKE1y+XK9f4tFrdTg/Lda1adKkie22d3Rc563SJJf2BsuSJYvkzJnTrkeHLdTj2gZN6OnSlzbYFdzBivYQ0/tq0SSn2uswfYl93zsIlUsQQACBBBH4z3/+Y4Y91Z9169ffGiUhQSqmEgQQQCDMBEhEhdkDp7kIIICADvWi41x7fns1T548onMQlC5d2sy78OCDD5oPNO5ETHs/6Wvjxo2iw+B4Fp1sXIf7oSAQbAI6F4YO2zR37jz5668/TfiZM2aTp1u+KI9XTtjhJddu/Ea+W/21bNp2a7ijhypUlmc7tpV69f0/LGCwPSfidaaAzs+kc0bt3LnTBOivZNQbb7whOqyOlj59+kjfvn3NenR/aM9dHWpMEwvTpk0zw5zpcGcUBBBAAAEEEAg/Af23gM4fqP9fdp0zMPwkaDECCCAQPwESUfHz42oEEEAgKAX0m6xz5syRb7/9VlavXu21DfoNY01KaUJKJ7/Wb4PrK0eOHGap33z966+/7JfOnaDfENOeV/pho2fJnTu3mdxa54vQoXEoCASzwG+//SY6F8m8efNl27atpik5suWR6lWaSdGC5aRk8QcldcrYh6TyNNi5Z4vs2fejrN24RH7de6ve0vc9KO2eflpaPpWwiS7Pe7ONgD8E9PdDv379ZOnSW3OdJXYyynUoQB0KVu8XW+nevbvpxTtw4EAzHJzOt6hFf89lypQptss5jgACCCCAAAIhJGD9W0L/Txzd/51DqLk0BQEEEEg0ARJRiUZLxQgggEBwCOjQNosXLzbzZmzatClBg9YhYpo1ayb169cn+ZSgslTmJAGdc0bna9rmMnlxypSpbyakykixIuXl/pKVbr4eln8uX5bL/1y4ubwkl/65KJeuXLy5fVH27v9Jft65Tvbs/1H++vvWcEfavpIly0j79k9L69YtndRcYkEgQQQ0GfXVV1+ZuhIrGWV9cKQ38TUJZTVO54jSORb1W9AfffSR/cGTJqXy5ctnncYSAQQQQAABBEJcQOcJ1B7VBQoU8DraR4g3n+YhgAACCSZAIirBKKkIAQQQCH4BnYNAP0zXuQ9OnjwpOgyZrutSX1euXDFzAeh8APrSeQLOnTtn5lXQoQp03gKdY0F7P+kY2lWrVg1+FFqAgI8CmshdsWLFzWH75pq/Gz5eFuW0mjVrik5Sr0sKAqEsoIkiHTbv7NmzNxOvJWX69OmSIUOGBGlyfJJQVgBvv/22mVtRe0atW7fO9CLWY/PmzZMyZcpYp7FEAAEEEEAAgRAW0H/b9+rVSwoVKmT+rR/CTaVpCCCAQKIKkIhKVF4qRwABBBBAAIFwFFizZo3otyf1w3BfiiZudY42ElC+aHFOKAns2LFDXnjhBTNvVN68eeWDDz6QihUr3nETdei/ESNGyPjx400dce0J5Xlj7Q01fPhwadKkiVy7ds0M2afnTJkyRapUqeJ5OtsIIIAAAgggEGICCxculG7duknRokVl2bJlIdY6moMAAgj4T4BElP+suRMCCCCAAAIIhKHAoUOHRF+HDx82S11PlSqVlCpVSooVKyaFCxc287CFIQ1NRsAIeM4b1bdvXzMETlx5NKnVuXNnu0difJNQ1v0nTZokgwcPNr19K1euLDNmzDCHPv30U6lbt651GksEEEAAAQQQCEEBHYa7S5cuUqJECVmyZEkItpAmIYAAAv4RIBHlH2fuggACCCCAAAIIIIAAAjEIuM4bpb2itHeU9pLypWgvqA8//NA+9dlnn5XXXnvN3o7vypw5c6R3796mmg4dOsiECRPMusbYvHnz+FbP9QgggAACCCDgUAHtBdWpUycpXbq03TPaoaESFgIIIOBogaSOjo7gEEAAAQQQQAABBBBAICwENKkzZMgQ01adk6levXqxDm+pvaD0PNcklCaGEjIJpQE1bdpUvvjiCxObJqG6d+9u1jV5NnHiRLPOHwgggAACCCAQegLJkyc3jUqWLFnoNY4WIYAAAn4UIBHlR2xuhQACCCCAAAIIIIAAAtELdOzYUT777DNJnz696JB9/fv3l1atWtnD7VlXWnNBaRJKk1FW0SSUJrQSo9SoUcMkxjJkyCBjxowxsel9NHmm2xQEEEAAAQQQCD0BKwFlLUOvhbQIAQQQ8I8AQ/P5x5m7IIAAAggggAACCCCAgI8CmlzSYXCOHTtmX9GiRQupXbu2aG+pmTNnmkSVffDmSmImoVzvs2vXLjNXhM73NnToUBk4cKA53KdPH9H5rSgIIIAAAgggEDoCa9askTZt2shDDz0Ua0/t0Gk1LUEAAQQSXoBEVMKbUiMCCCCAAAIIIIAAAgjEU0B7PenQd0uXLo21Jn8loaxANEHWo0cP2bJli6xevVoqV65sDpGMsoRYIoAAAgggEBoCERER0rp1a9H5K6dPnx4ajaIVCCCAQAAEGJovAOjcEgEEEEAAAQQQQAABBGIW0CHwPv/8c3uoPm9n6xB+77//fqINx+ftnrovT548MnnyZKlevbqdhNL9I0aMcJuvSvdREEAAAQQQQCB4Ba5evWqCv3HjRvA2gsgRQAABBwiQiHLAQyAEBBBAAAEEEEAAAQQQ8C5Qp04dWbt2rWhvI00AadEElPaCWrJkieiQfYEo6dKlkwkTJkjTpk3dbk8yyo2DDQQQQAABBIJa4Nq1ayZ+ElFB/RgJHgEEHCCQ3AExEAICCCCAAAIIIIAAAgggEK2A9o7S+Zf0pUP26bZTysiRI008kyZNskPSZJQW5oyySVhBAAEEEEAgKAWuXLli4r5+/XpQxk/QCCCAgFME6BHllCdBHAgggAACCCCAAAIIIBCrgJOSUFawb775ppkzytrWJT2jXDVYRwABBBBAIDgF6BEVnM+NqBFAwHkCJKKc90yICAEEEEAAAQQQQAABBIJMYMCAAfLqq6+6Ra3JqA8++MBtHxsIIIAAAgggEDwCzBEVPM+KSBFAwNkCJKKc/XyIDgEEEEAAAQQQQAABBIJE4LnnnpNhw4a5RTtq1Ch5++233faxgQACCCCAAALBIWAlooIjWqJEAAEEnCtAIsq5z4bIEEAAAQQQQAABBBBAIMgE2rRpI2PGjHGL+tNPP5UhQ4a47WMDAQQQQAABBJwvYCWimCPK+c+KCBFAwNkCJKKc/XyIDgEEEEAAAQQQQAABBIJMoGHDhjJp0iS3qCdOnCgvvvii2z42EEAAAQQQQMDZAtYcUSSinP2ciA4BBJwvQCLK+c+ICBFAAAEEEEAAAQQQQCDIBKpWrSpz5851i3r69OnSu3dvt31sIIAAAggggIBzBa5cuWKCu3HjhnODJDIEEEAgCARIRAXBQyJEBBBAAAEEEEAAAQQQCD6BsmXLyooVKyRp0tv/7ZozZ47oXFIUBBBAAAEEEHC+AD2inP+MiBABBIJD4Pb/iIIjXqJEAAEEEEAAAQQQQAABBIJGoFChQrJhwwbJkiWLHfOSJUukffv29jYrCCCAAAIIIOBMAWuOKHpEOfP5EBUCCASPAImo4HlWRIoAAggggAACCCCAAAJBKJA9e3aJiIiQggUL2tGvXLlSWrdubW+zggACCCCAAALOE7B6RJGIct6zISIEEAguARJRwfW8iBYBBBBAAAEEEEAAAQSCUCB16tTy3XffyQMPPGBHr8kpklE2BysIIIAAAgg4ToA5ohz3SAgIAQSCVIBEVJA+OMJGAAEEEEAAAQQQQACB4BP4+uuvpVatWnbgJKNsClYQQAABBBBwnIDVI+r69euOi42AEEAAgWASIBEVTE+LWBFAAAEEEEAAAQQQQCDoBcaNGyctW7a020EyyqZgBQEEEEAAAUcJMEeUox4HwSCAQBALkIgK4odH6AgggAACCCCAAAIIIBCcAsOHD5cuXbrYwZOMsilYQQABBBBAwDECJKIc8ygIBAEEglyARFSQP0DCRwABBBBAAAEEEEAAgeAUGDhwoLz00kt28CSjbApWEEAAAQQQcISAlYhiaD5HPA6CQACBIBYgERXED4/QEUAAAQQQQAABBBBAILgFunXrJu+8847dCJJRNgUrCCCAAAIIBFzAmiMq4IEQAAIIIBDkAiSigvwBEj4CCCCAAAIIIIAAAggEt8BTTz0ln332md0IklE2BSsIIIAAAggEVMDqEXXjxo2AxsHNEUAAgWAXIBEV7E+Q+BFAAAEEEEAAAQQQQCDoBerUqSMzZsyw20EyyqZgBQEEEEAAgYAJWIkohuYL2CPgxgggECICJKJC5EHSDAQQQAABBBBAAAEEEAhugYcffliWLVtmN0KTUS1atLC3WUEAAQQQQAAB/wpYiSj/3pW7IYAAAqEnQCIq9J4pLUIAAQQQQAABBBBAAIEgFShatKhs2rTJjn7Dhg3SpEkTe5sVBBBAAAEEEPCfgDVHlLX03525EwIIIBBaAiSiQut50hoEEEAAAQQQQAABBBAIcoHs2bPLvn37JHny5KYl27Ztk7p16wZ5qwgfAQQQQACB4BO4cuWKCZo5ooLv2RExAgg4S4BElLOeB9EggAACCCCAAAIIIIAAAiYJpcmoLFmyGI2dO3fK448/jgwCCCCAAAII+FGAnlB+xOZWCCAQ0gIkokL68dI4BBBAAAEEEEAAAQQQCGaBrVu3SoECBUwTNDFVuXLlYG4OsSOAAAIIIBBUAtYcUdevXw+quAkWAQQQcJoAiSinPRHiQQABBBBAAAEEEEAAAQRcBFauXCllypQxe44cOSIPP/ywy1FWEUAAAQQQQCCxBKxEFEPzJZYw9SKAQLgIkIgKlydNOxFAAAEEEEAAAQQQQCBoBebNmydVqlQx8Z84cULKlS0btG0hcAQQQAABBIJFwEpE0SMqWJ4YcSKAgFMFSEQ59ckQFwIIIIAAAggggAACCCDgIjBlyhRp0KCB2XP6zBkpVaqUy1HfVvfs2SMRERG+ncxZCCCAAAIIhLmANUcUPaLC/I1A8xFAIN4CJKLiTUgFCCCAAAIIIIAAAggggIB/BMaOHStt2rQxNzt37pwUKlQoTjcePHiwtG7dWn7++ec4XcfJCCCAAAIIhKOA1SOKRFQ4Pn3ajAACCSlAIiohNakLAQQQQAABBBBAAAEEEEhkgWHDhknXrl3NXfQDsoIFC4r1QVlsty5QoIA5ZerUqbGdynEEEEAAAQTCXsD6/UoiKuzfCgAggEA8BUhExROQyxFAAAEEEEAAAQQQQAABfwu88sor8uKLL5rb6rBBZcqUkfPnz8caRrNmzcw5moiiV1SsXJyAAAIIIBDmAiSiwvwNQPMRQCDBBEhEJRglFSGAAAIIIIAAAggggAAC/hPo3r27vPXWW+aGOkzfY489Jn/++WeMATz44INSs2ZNcw69omKk4iACCCCAAAJizRF1/fp1NBBAAAEE4iFAIioeeFyKAAIIIIAAAggggAACCARSoG3btjJq1CgTwh9//CENGzaU3377LcaQ2rdvb47TKypGJg4igAACCCAgV65cMQoMzcebAQEEEIifAImo+PlxNQIIIIAAAggggAACCCAQUIEmTZrIpEmTJFWqVHLkyBFp06aNHD16NNqYtOdU06ZNzXF6RUXLxAEEEEAAAQTsORjpEcWbAQEEEIifAImo+PlxNQIIIIAAAggggAACCCAQcIGqVavK9OnTJVu2bLJnzx557rnn5MCBA9HGRa+oaGk4gAACCCCAgC1gDc1HjyibhBUEEEDgjgRIRN0RGxchgAACCCCAAAIIIIAAAs4SKFeunMyYMUMKFCggv/zyi3Tp0kV27NjhNcjy5cvL008/bY7RK8orETsRQAABBBCwe0RBgQACCCAQPwESUfHz42oEEEAAAQQQQAABBBBAwDEChQoVkpkzZ0rZsmVl9+7d0rFjR1m/fr3X+HR+KR3OTxNRGzZs8HoOOxFAAAEEEAhngatXr5rmMzRfOL8LaDsCCCSEAImohFCkDgQQQAABBBBAAAEEEEDAIQLZs2c3yagqVapIZGSkdOrUSb799tso0RUvXlzatWtn9n/xxRdRjrMDAQQQQACBcBdgaL5wfwfQfgQQSCgBElEJJUk9CCCAAAIIIIAAAggggIBDBFKmTClTpkyR+vXry99//22SUXPnzo0SnfaKypI5syxevNhrsirKBexAAAEEEEAgjAToERVGD5umIoBAogqQiEpUXipHAAEEEEAAAQQQQAABBAIn8PHHH0urVq1EhxTq1auXSU65RpMvXz55+mYySsukSZNcD7GOAAIIIIBA2AtYiaiwhwAAAQQQiKcAiah4AnI5AggggAACCCCAAAIIIOBkgffee086d+5sQhw4cKBocsq1tG/fXvLmzSsrV66U2bNnux5iHQEEEEAAgbAWsBJRzBEV1m8DGo8AAgkgQCIqARCpAgEEEEAAAQQQQAABBBBwssCgQYOkf//+JsR33nlHhg8fboebLVs2eeaZZ8z2l19+aXpP2QdZQQABBBBAIIwFmCMqjB8+TUcAgQQV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AAEEEEAAAQQQ8EGArz/5gMQpCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACcRcgERV3M65AAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBDwQYBElA9InIIAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBB3ARJRcTfjCgQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAR8ESET5gMQpCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACcRcgERV3M65AAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBDwQYBElA9InIIAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBB3ARJRcTfjCgQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAR8ESET5gMQpCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACcRcgERV3M65AAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBDwQYBElA9InIIAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBB3ARJRcTfjCgQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAR8ESET5gMQpCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACcRcgERV3M65AAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBDwQYBElA9InIIAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBB3ARJRcTfjCgQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAR8ESET5gMQpCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACcRcgERV3M65AAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBDwQYBElA9InIIAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBB3ARJRcTfjCgQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAR8ESET5gMQpCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACcRcgERV3M65AAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBDwQYBElA9InIIAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBB3ARJRcTfjCgQQ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h8WLF7e3o1tZtWqVzJ071/i6Pkf94F7/junQjV26dIkxCbxs2bIoPTqs+1WuXFkGDRpkNteuXStDhw41CWrruC51uLE33nhDihYtau+eOHGi/Pe//7W3tceOvg98SYxoez7++GP7Wl2ZPn26ZMyY0W0fG7cFvCWNtPdOpkyZzEmnT5+WNm3a3L7g5po+n8yZM5t9p06dcjumG569rFxPSOyf8/PmzRMdDta195G+px966CHR96T+fYnp95BrrAm5rgkLzxLb+1J7QLVr187zshi3E/LnpibUCxUq5HY/tUyfPr0Z0lWH6NWfw9pjK7a2aCWJ/fvONVD11p8l27ZtE/33h+v7Qc/TdjRo0EB0mFr9OeTLzxerfv19PW3aNPn000/l0KFD1m6zVBOdg1F7jlMQQCA4BEhEBcdzIkoEEEAAAQQQQAABBBBAwKtAgQIFouzXb9rrfETein6A//zzz4vnEE/WufpBkr70g+YXXnhBunfv7tcPE7ds2WKFYpaafEiRIoVZ1+SL6wf5mszR5JJVtG2ux/XDL6tERkZG+XDeOqZLHepQv21do0YN1932utaryQJNyAwYMMDeb61ogsL13tZ+b0tNasRUNBnmS12xnfPAAw/EdBtzTJN73tpjXajm+tKE4PDhw01yzzrmz6V+CK/PesOGDV5vq++b1q1bmw9Ef/31Vzc/TXB6K9pzwvVD0+jO02s1OapJHtei87XFVPS4mo0bN87radq7RevU15w5c0wiUxOe3squXbvc2uR6jtWLQp+R/p31VvQeOn+cJkFr1qxpTtEkhut7SNc1Efnggw96q8JtnyYhXK/VuH35gNytkjDbyJo1a5QW//HHH3YiSj9odzXVk3VfTImo6MwT++e8Pn9NAngWfU/rlwH0pcn8kSNHBjyBrTF+//33XpPRnvH7up3QPze1t5pnUUt96e9q6+eeJnX0Swpt27a1fy96Xqfbif37zrqnzlX58ssvR/vvCT1P26BJan3pFzn0Z2J071urXmupic7ovuChP6s02amv119/nR5SFhpLBBws4Mw+9g4GIzQEEEAAAQQQQAABBBBAwOkC0Q0tpB9Uay+i6JJQnu3697//bfco8jyWGNvePpzSXgJW8RxyyTWJoOecOHHCOtUs8+XL57Yd04YOY/bKK6/EdIo59tFHH5mh1WI9MQhO0B5EMSWhXJug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7ZQFo0obarBSDUpefPHFKZ83rFn14qF+dRizZvtFXtX0f//3f5vuGvOinHjiiU23NctceumlS9nXXXddOuKII0p5dV3ZfPPNS1WLgOcdd9yRbrrpplJ+9J6Koc2kzgWK8z/F0XfddddAIYsuumi2HMHI/Lsff5/VXifNhrmrSzsfv0PVv8Fll1124Bp7vRBtQCdB+ttuu62hStVgcuzQi3az+vLIbLPN1lCXPCOGqL3kkkvy1Un2udRSSzWc+9///d9LvfgadpBBgACBjwUEonwNCBAgQIAAAQIECBAgMBkKrL766qn6sOzoo49ueAAYlxYPjA844IDSVcaDws022yydf/752fwTpY2TeKX6kPXmm28eqFEegIqh9/L5TGK4omJq9kC2uH1yXM6vu1j3uKfVAEFxe6vlmHskAgnFdOONN2ZDPoV19ICpS9pyyy0bqvK9732vae+/eGC/5557NuzfKqMasIxrP+WUU1KxN0TMSfWjH/2oNJRaq/Ly/B122KHUoyXyf/GLX6S9994761mU71fHz5iDbYUVVihV7ZxzzknXXnttKS96bUjDE6i2b7fccstAQcXgaHG5uE/sXC0j8iZ1Ox/DCR500EHpZz/7WVRnIEVgNoa/m1gphhmNc8Zn/E0PFpSK34599tmnVLWwbTY/XS/azWq7HkP1xXxQ1RTtUAyRWYeeR/ESS/7bm9fz1ltvTeuuu24WtM7zmn3Gb0udfl+a1VEeAQK9E5g0/b17dz1KJkCAAAECBAgQIECAwCdCIObP+f73v59233330vVGz4199923lBcr2267bbrwwgtLb9/HUFsxr9TBBx+cxowZk+Kh2CyzzJLioVfMQRTzfNx7770pAlxjx45tKLNXGdWHrPmwVTFcVcxRkacIJERvgurDubwnQb5ffMak9MV5VqJnTTHFA8v999+/mJU23XTT1M6b/FdffXW6/vrrS8fmK88880y+OPD585//PBt+ayDjHwt77bVX0wegsTkCb3GPbr/99oHDIpi49dZbp+hFsdBCC6XZZ589e4gZb9lHb7fYHvVqNozegQcemK688sqBsmIhyt5qq62yHnRLLrlkip5p008/fYqhtvJ5bMI7hpOK4cMmRopeCGuuuWYK4zxFPeOhZ8yjEtf95ptvZgGeGAKvk7T88ss37B7BvV//+tcp3vqPwNb48eMzx+rc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AgQIECBQK4FJ+xpTrShUhgABAgQIECBAgAABAgQIECBAIHrabbPNNiWIFVZYQRCqJGKFAAECBAgQaFdAIKpdKfsRIECAAAECBAgQIECAAAECBPpYIIZcO+2001LMQVadX2zfffft4yt3aQQIECBAgEAvBf7/ANK9PIOyCRAgQIAAAQIECBAgQIAAAQIEaiXwyiuvpEceeSSbzyXmdbnjjjvS+PHjs3niqhXdZZdd0pgxY6rZ1gkQIECAAAECbQkIRLXFZCcCBAgQIECAAAECBAgQIECAQP8InH766WncuHFDXtCaa66Z9tprryH3swMBAgQIECBAoJWAoflaycgnQIAAAQIECBAgQIAAAQIECPSpwIwzzjjkle22227puOOOS1NO6T3mIbHsQIAAAQIECLQU8F8SLWlsIECAAAECBAgQIECAAAECBAj0p8AMM8zQ9MIiQPWNb3wjbb755mmRRRZpuo9MAgQIECBAgEAnAgJRnWjZlwABAgQIECBAgAABAgQIEKi1wD777JNef/31gTous8wyA8sWJgjMNddcaezYsWn06NHp85//fPrCF76QFlpoobTAAgukaaaZZsKOlggQIECAAAECIxSY4qOP0wjLcDgBAgQIECBAgAABAgQIECBAgAABAgQIECBAgACBBgFzRDWQyCBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEOiGgEBUNxSVQYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0CAgENVAIoMAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKAbAgJR3VBUBgECBAgQIECAAAECBAgQIECAAAECBAgQIECAQIOAQFQDiQwCBAgQIECAAAECBAgQIDBB4J133kmvvvpqev/99ydkWpqoAm+99VZ64403Juo5nYwAgeELaDeHb9etI9977730+uuvp48++qhbRSqHAAECwxaY4uPGSGs0bD4HEiBAgAABAgQIECBAgEA/CUSw6fbbb0+XXXZZ+sMf/pCeffbZ9Pe//z27xMsvvzwtuuii/XS5k821LL744gP3Yf75509LLrlkWm+99dKqq66app9++snmOlSUQD8KaDfreVd33333dPHFF2eVm3POOdPCCy+c1l577bTmmmum2WefvZ6VVisCBPpWQCCqb2+tCyNAgAABAgQIECBAgACBTgTGjx+f9tlnn/T44483PeyOO+5Io0aNarpNZm8FVlxxxSwoWD3LjDPOmA444IC02WabpU99yqAvVR/rBHotoN3stfDwy4/fs7PPPrtpATvvvHPaY489BPKb6sgkQKAXAv4rrReqyiRAgAABAgQIECBAgACByUbgww8/TAceeGDacsstWwah4mJmnXXWyeaaulnRc889N0UgKP/37W9/u5vFt1XW3HPP3XS/6K229957p6233trQfU2FZBLojYB2c3DXOrSbc801V8tKHnvssWns2LHp4YcfbrmPDQQIEOimwJTdLExZBAgQIECAAAECBAgQIEBgchM45JBD0kknndS02tHjZrHFFsuG5GvV4+a6665LL7zwQun4FVZYIc0777ylvOLKDTfckJ577rmBrBgqqa6BrjfffLPUG2m66aYbqPfEWthggw3SNNNMkz00jeESq+nmm29OO+ywQzrllFPSpKhftT7WCfS7gHZz8Dtch3ZzzJgxWbDpySefTA899FBDhaMt3WKLLdIFF1yQ5plnnobtMggQINBNAYGobmoqiwABAgQIECBAgAABAgQmK4HTTjstnXjiiQ11jt4/Mb9GfE455eD/1zmOv/HGG0tlxFBxP/vZz0p5xZVTTz01XX311QNZl156aW0DUQOVnIQL0Qsr74l13333pf/5n/9J0eOgmGKIsB/+8IfpyCOPLGZbJkCgywLazS6D9qi4lVdeOcW/SM8880w677zz0uGHH1462//93/+lbbbZJl1xxRWC+CUZKwQIdFvA0HzdFlUeAQIECBAgQIAAAQIECEwWAs8//3zaf//9G+oa82qceeaZ6atf/eqQQaiGg/+R8Zvf/Ca99NJLrTbLH4HA4osvnj1MPeGEExpKiTf7b7nlloZ8GQQIdEdAu9kdx4ldyujRo7OXK6666qqG3k8xL+Lxxx8/savkfAQIfMIEBKI+YTfc5RIgQIAAAQIECBAgQIDA/xcYN25cA8Uuu+ySdt111/TpT3+6YVunGeeff36nh9i/A4G11lorHXPMMQ1H7LfffumDDz5oyJdBgMDIBbSbIzeclCUstNBCKXrkxrCzxXTEEUekp556qphlmQABAl0VEIjqKqfCCBAgQIAAAQIECBAgQGByEHjllVcahnabf/75095779216sfwVR9++GHXylNQo8B6662XNt1009KGRx99NN12222lPCsECIxcQLs5csM6lDDffPOlAw44oKEqMXSfRIAAgV4JCET1Sla5BAgQIECAAAECBAgQIFBbgcsvv7yhbttvv3361Ke693+TY7gjw8Q1MHc9Y9ttt20o88ILL2zIk0GAwMgEtJsj86vT0RtuuGFDr6izzjorffTRR3WqproQINBHAt37L+w+QnEpBAgQIECAAAECBAgQINDfAhdddFHDBX79619vyBtpxhlnnDHSIpoe/95776Xo+RMTzXcrvfzyy+nBBx/sapl53d59992svi+++GKe1bXPZZddNi2xxBKl8uKB6vvvv1/Ks0KAwMgEtJuNfr1sN6NH7ZNPPpkNmdft3rXTTTdd2mabbUoX9Oyzz6Z77rmnlGeFAAEC3RKYslsFKYcAAQIECBAgQIAAAQIECEwOAjF/0M0331yq6nbbbZdmmmmmUt5wV2Lujb///e/Z4Zdcckl6/vnn0xxzzDHc4gaO+9Of/pTOPPPMdPfdd6c///nPA/mf+9zn0tJLL52+8pWvpOjVNeWU7f9f/Ztuuimdc845KcqOHlx5ijK/853vZOXleZ1+3nDDDSkeXN93332l+obPUkstlVZeeeX03e9+N0099dSdFt2wf9R1zz33LOX/9a9/TQsssEApzwoBAsMT0G5OcOtluxkvGJxyyilZu3n77bdPOOnHS9HOL7PMMmm33XZLs88+e2nbcFa22mqrhnn2or2O9lkiQIBAtwX0iOq2qPIIECBAgAABAgQIECBAoNYC8YZ5NcUwRd1KEQwqpnPPPbe4mi138nZ77HvCCSekjTfeOJ199tmloE4UFr2irr322nTwwQdn8yU1u76GCnycEWVuvfXWWbCoGITKyzzssMPSXnvt1fFQTW+//XY66KCDsrftf/Ob3zTUN4J0EQgcN25cijmeikG1ZvVsJ2/ttddu2O2BBx5oyJNBgMDwBJq1K9rNCcH7UI22eLjtZhwfLwWsvvrq6dRTT03VIFRsv+uuu7Jtsc+ll14aWSNK88wzT1puueVKZdx///2ldSsECBDoloBAVLcklUOAAAECBAgQIECAAAECk4VAswDFnHPO2bW6r7POOqW5N04++eQRDRO3yy67ZEGmdioYDyrj/EMNr3TggQe2VWb06LrxxhvbOXW2zxtvvJE22WSTdOKJJ7Z1zEMPPZTVN97CH0mafvrpS+ZRVjcCXCOpk2MJ9JOAdjOlXrWb8T3ZZ5990t57793WVyaC+d/73vfSr371q7b2H2ynueeeu7T53nvvLa1bIUCAQLcEBKK6JakcAgQIECBAgAABAgQIEJgsBJrNqzRq1Kiu1T2Gmtthhx0GyovzxTB1w0nXXXdduuyyy5oeuuCCCzYEX2LHeEgZvaNapcceeyyddNJJDZsjGLfppptmPa+i7DxdffXV+eKQnxF0axYAiqH+xowZkw0t1ayQwerbbP9mefPOO28p+4UXXiitWyFAYPgC2s3etZu33npr1tu1endiGNNoN6PXUixX03/+53+mV199tZrd0Xr1JYyYJ0oiQIBALwQEonqhqkwCBAgQIECAAAECBAgQqK1APn9TXsF4wDfNNNPkq1353HzzzUvlDOfN9RiSLx40VlPM6xHBnggQxdvrv/71rxseUo4fP75l8KtZb6WYY+mWW25JP//5z9NRRx2VlX300Uc3lFutS3E9HlQffvjhxawUAagYZurOO+9M5513Xrr44ouzOa422GCD0n4xVF8E3UaSRo8eXTq8ep9LG60QINCRQPXvSbuZsnn0RtpufvTRR+nQQw9tuBf7779/1lZGu3nBBRdkw/JVe0zFPTnmmGMaju0kozp/4euvv97J4fYlQIBA2wICUW1T2ZEAAQIECBAgQIAAAQIE+kGg+qCt2pOmG9f4xS9+MY0dO3agqJjDqdkcKwM7NFm45pprGnoXRY+leGgZQ9FFmmKKKdJKK63U9G36CCpV09/+9rd0+umnl7LXX3/9FA89o6xi2mijjdIhhxxSzBp0OeacqqZ4gLraaquVsj/zmc+kX/7ylw35MT/KSFL1zf7XXnttJMU5lgCBgoB2szft5vXXX58F6gvU6b/+67+yINenP/3pgewpp5wy7bbbbmnfffcdyIuF4bzkUCygGoiK4FYncxgWy7JMgACBwQQEogbTsY0AAQIECBAgQIAAAQIE+k7gzTffLF3T7LPPXlrv1so222xTKqrTQEuzeZNiXpBqwChOssQSS5QCX5EXvZA++OCDWBxIzeZ52W677Qa2Vxc23HDDNP/881ezm67fcccdpfx4e3+eeeYp5RVXqm/3P/roo8XNHS9H76tiivmqJAIEuiOg3Sw7dqvdjHn9immVVVZJ0e62Sttvv33W0zTfHoGjl19+OV/t+LPabkYBb7/9dsflOIAAAQJDCQhEDSVkOwECBAgQIECAAAECBAj0lcBUU01Vup7qkFOljSNYWXXVVUsPDGP+pHfffbftEh9//PHSvksvvXRaaKGFSnnFlS233LK4mi1X50mqzv8RvYi+/OUvNxyXZ8Qb+csss0y+OujnI488Utq+7rrrltarK4svvngpK4YbjGGqhpuqD8pjri6JAIHuCGg3Jzh2s9184oknJhT88VJ12NLSxo9XpptuurT88suXsp9++unSeicr1XYzjtV2diJoXwIE2hWYst0d7UeAAAECBAgQIECAAAECBPpBoDrpe6dD5rVrEEMp7bDDDmncuHHZIRHwuuqqq1IMhddO+utf/1ra7Utf+lJpvbrSrPdRPKAsDln3zDPPlA6bb775mvawKu7UrNzi9lh+6623UswRVUz33HNPatarq7hPdfnFF19Ms802WzW7rfVqkG3mmWdu6zg7ESAwtIB2c4JRt9rNKLHaE/Spp57K5tKbcLbGpYcffriUGb9h0St2OOn5559vOCx+uyQCBAh0W0DL0m1R5REgQIAAAQIECBAgQIBArQWqAYoIYMScGJ/6VPcHDdl8880HAlGBctppp7UdiHrwwQdLjp///OdL69WVUaNGVbNSPNQcM2bMQH71zfnRo0cPbGu1MMsss7TaNJBfLTc27LHHHgPb21149dVXuxaIqj44b7cO9iNAoFFAuznBpFvtZpRYbeePPvroCSdqc+mVV15pc8/G3aq9ZpsN1dd4lBwCBAh0LtD9/8ruvA6OIECAAAECBAgQIECAAAECE01gpplmajjXSObYaCiskBETwRd7QI0fPz5Vh7Ar7D7oYnHi+mY7NgukVeeIqs6b1K1J6UcypF7xWqaddtriakfL1Z5t1QfnHRVmZwIESgLazQkc3Wo3J5Q4sqWRtJvPPfdc6eSf+cxnSutWCBAg0C0BgahuSSqHAAECBAgQIECAAAECBCYLgXnnnbehnn/7298a8rqVsfXWW5eKOuuss0rrrVaq9Ww2hFLx2OrQeLFtrrnmKu5SGqYvNlQDU6WdO1iZe+65O9i79a4x/8lwUjwYrg7NN9RQhsM5j2MIfFIFqu1ROGg3R/5tWGSRRUZcyPTTTz/sMqrDtS666KLDLsuBBAgQGEzA0HyD6dhGgAABAgQIECBAgAABAn0nsNBCCzVc091335268UCwoeCPM1ZaaaUU8yw9/vjj2eYIRC222GLNdi3lxYPfe++9dyCvOqn9wIZ/LFQfKEZ2dQip6nr1bfhqme2uxxv5xWuM44477ri05pprtltEtt9w5yZ54IEHGs7jgWoDiQwCwxbQbk6g61a7GSXG787tt98+UPj222+ffvzjHw+st7MwVG/ZVmW8++67pXPHfu38NrUqTz4BAgQGE9AjajAd2wgQIECAAAECBAgQIECg7wRmmGGGLGhSvLBTTz21uNrV5Rgyb9tttx0o8+9//3u69dZbB9ZbLURgp5hiWL9mwaZ8n4suuihfHPicc845B5ZjodpD6q677krNelIVD4r6tpMWXHDB0m4RcIvAUif/SgV0sHLmmWc27N3swXnDTjIIEGhLQLs5gamb7eb8888/oeCPl84999z0zjvvdNRuTjHFFKUy2l256qqrUrV9F8BvV89+BAh0KiAQ1amY/QkQIECAAAECBAgQIEBgshf42te+VrqG6Hn0xz/+sZTXzZVNN9204+KaBVJOO+20puVEgOr8888vbYvA0FRTTVXK+8IXvlBaj5XzzjuvIa+YceeddxZXWy4vvfTSpW3XXXddOuKII0p5vVh5/fXXU9Ul6mKuk15oK/OTLKDdnHD3u9VuLr744hMK/XgpAkM77bRTFowqbejByimnnNJQ6pgxYxryZBAgQKAbAgJR3VBUBgECBAgQIECAAAECBAhMVgIbbLBBQ33POOOMhrxuZcw666xps80266i49dZbr2FOp2OOOSadeOKJpXKefPLJ9I1vfKOUFyu77757Q14Mu1QNGB1yyCEpgkbV9NFHH6Xo1XT11VdXNzVd32GHHdLnPve50rZf/OIXae+9906PPPJIKb+bKxdeeGFDcZ1aNxQggwCBBgHt5gSSbrWbK664YlprrbUmFPzx0o033pi++c1vpptvvjlFO9yL9Je//KWhZ+76668vgN8LbGUSIJAJTPFxg9abFg0wAQIECBAgQIAAAQIECBCoqUD8X+F//ud/Hpi3Ka/m73//+1QdKinf1urzW9/6VvbgMN8ewx016810xx13pFY9oy699NJUfTM+yrvgggvSnnvumRc98BnD9i288MLptddeSzFkXzVFb6grrrgiNZs75JprrkkRNKqmeMi8zDLLpJjv6YUXXki/+93v0qOPPlrdLfMJp2apVdmxb7xpH7YxPGAMVxh1j55cf/7zn9NMM82ULr744mZFDpoXvQciYJfPv5XvHNajRo3KV30SINAFAe1mb9rNZ599NkVAqlmKNnPJJZfM2s3pp58+vfHGG+nFF19MDz74YIohAmN+w+H0/owXFapt7gknnNAQFGtWJ3kECBAYjsCUwznIMQQIECBAgAABAgQIECBAYHIWiDk1oqfObrvtVrqM7bbbLns499nPfraU342V5ZdfPkWA6KGHHmq7uA033DDrARVDBxZTBF6qwZfi9pjsvlkQKvYZO3ZsWmKJJVK1zAg8xb9qarZvdZ98fY011siCbdVhAmP77bffnv3L961+xkPuTuY6ef/999Mee+zR4LDjjjsKQlVxrRPogoB2szftZszlN27cuOw3qXqb4mWAZi8E5Ps99dRTHQeijj322IYgVLTz0X5LBAgQ6JWAofl6JatcAgQIECBAgAABAgQIEKi1QPQAqr6FHsGdjTbaKHvTvBeV33777Tsqdsopp8wmr99iiy3aOi6GxovJ7ldbbbWW+8fD5DPPPDMLSLXc6R8bYuL6H/7wh0PtVtoe80IdffTRDcP0lXZqshJv+bebogfB1ltv3TBs4Iwzzpi+//3vt1uM/QgQ6FBAuzl2SLHhtJvRxv/2t79tGDp1qJM98cQTQ+0ysP2tt95KBxxwQDr00EMH8vKFgw8+uOXLC/k+PgkQIDASAYGokeg5lgABAgQIECBAgAABAgQmW4EIyBx22GEpghfFlAejomfNJZdckg0d9/LLLxd3KS3HcEntpujh1CxNN910zbKzvCg/3pY/6qijWj6kjDfqN9988ywws8IKK7QsK98QQznFXFPRKyyG+WuW1lxzzXTyySd3PFRhuEYw74Ybbsh6nMWb9kOluAeDGcdwVDHPVMxltc8++2QBxGZDEkYAbOaZZx7qdLYTIDBMAe1mb9rNuB1LLbVUNhzrT3/602wo0+pvU7Nb9vrrrzfLzvLefffdFIGq2267LR1++OFp5ZVXTqecckrD/rvuumtadtllG/JlECBAoJsC5ojqpqayCBAgQIAAAQIECBAgQGCyE7jvvvtSvI0e8w21SvFAMParQ4oh6SJYFr2Cpplmmmw+quHMEVK8lpivKSavf/vtt9MMM8yQvvjFL6bZZpst2+WDDz7IzhcBsfxf9NTqJH344YfZvFNR73grP64h5qKKIRBjzqhZZpll0OIiKHjllVcOuk8E6jbeeONB97GRAIHuCGg3UzbPXS/bzbhTEaCPdjPa6Gifo92MOfVGjx6dtdEx316rFEGn6AE1WIo5DqM3VAQYJQIECPRSQCCql7rKJkCAAAECBAgQIECAAIHJQiAeJu61114N8yYVK//AAw9kDwGLeZYnjsA666yT9UxrdrYYjjB6jJnfpJmOPAK9E9Bu9s62GyX/5Cc/Sccdd1zLomLY1Qjyt5pPsOWBNhAgQGAYAq3D5sMozCEECBAgQIAAAQIECBAgQGByFFhkkUXSRRddlA466KC04IILNr2EF154oWm+zN4LPPnkkw0niQDULrvskq6//npBqAYdGQR6L6Dd7L3xSM7w9NNPNz180003zYY53XnnnQWhmgrJJECgFwJ6RPVCVZkECBAgQIAAAQIECBAgMFkLPPzww+mOO+7IhpOLAFQMJ/dv//ZvadSoUZP1dU2uld9///2z4fxmn332bDiqRRddNC2zzDIeok6uN1S9+1JAu1mv23raaaele+65J0XQPtrOeeedN6200kppsDkJ63UFakOAQD8JCET10910LQQIECBAgAABAgQIECBAgAABAgQIECBAgACBGgkYmq9GN0NVCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQL9JCAQ1U9307UQIECAAAECBAgQIECAAAECBAgQIECAAAECBGokIBBVo5uhKgQIECBAgAABAgQIECBAgAABAgQIECBAgACBfhIQiOqnu+laCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQI1EhCIqtHNUBUCBAgQIECAAAECBAgQIECAAAECBAgQIECAQD8JCET10910LQQIECBAgAABAgQIECBAgAABAgQIECBAgACBGgkIRNXoZqgKAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKCfBASi+uluuhYCBAgQIECAAAECBAgQIECAAAECBAgQIECAQI0EBKJqdDNUhQABAgQIECBAgAABAgQIECBAgAABAgQIECDQTwICUf10N10LAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBGAgJRNboZqkKAAAECBAgQIECAAAECBAgQIECAAAECBAgQ6CcBgah+upuuhQABAgQIECBAgAABAgQIECBAgAABAgQIECBQIwGBqBrdDFUhQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECPSTgEBUP91N10KAAAECBAgQIECAAAECBAgQIECAAAECBAgQqJGAQFSNboaqECBAgAABAgQIECBAgAABAgQIECBAgAABAgT6SUAgqp/upmshQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNRIQCCqRjdDVQgQIECAAAECBAgQIECAAAECBAgQIECAAAEC/SQgENVPd9O1ECBAgAABAgQIECBAgAABAgQIECBAgAABAgRqJCAQVaOboSoECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgX4SEIjqp7vpWggQIECAAAECBAgQIECAAAECBAgQIECAAAECNRIQiKrRzVAVAgQIECBAgAABAgQIECBAgAABAgQIECBAgEA/CQhE9dPddC0ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgRoJCETV6GaoCgECBAgQIECAAAECBAgQIECAAAECBAgQIECgnwQEovrpbroWAgQIECBAgAABAgQIECBAgAABAgQIECBAgECNBASianQzVIUAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0E8CAlH9dDddCwECBAgQIECAAAECBAgQIECAAAECBAgQIECgRgICUTW6GapCgAABAgQIECBAgAABAgQIECBAgAABAgQIEOgnAYGofrqbroUAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCMBgaga3QxVIUCAAAECBAgQIECAAAECBAgQIECAAAECBAj0k4BAVD/dTddCgAABAgQIECBAgAABAgQIECBAgAABAgQIEKiRgEBUjW6GqhAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIE+klAIKqf7qZrIUCAAAECBAgQIECAAAECBAgQIECAAAECBAjUSEAgqkY3Q1UIECBAgAABAgQIECBAgAABAgQIECBAgAABAv0kIBDVT3fTtRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEaiQgEFWjm6EqBAgQIECAAAECBAgQIECAAAECBAgQIECAAIF+EhCI6qe76VoIECBAgAABAgQIECBAgAABAgQIECBAgAABAjUSEIiq0c1QFQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAPwkIRPXT3XQtBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEaCQhE1ehmqAoBAgQIECBAgAABAgQIECBAgAABAgQIECBAoJ8EBKL66W66FgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAjQQEomp0M1SFAAECBAgQIECAAAECBAgQIECAAAECBAgQINBPAgJR/XQ3XQsBAgQIECBAgAABAgQIECBAgAABAgQIECBAoEYCAlE1uhmqQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDoJwGBqH66m66FAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAjAYGoGt0MVSFAgAABAgQIECBAgAABAgQIECBAgAABAgQI9JOAQFQ/3U3XQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCokYBAVI1uhqoQIECAAAECBAgQIECAAAECBAgQIECAAAECBPpJQCCqn+6mayFAgAABAgQIECBAgAABAgQIECBAgAABAgQI1EhAIKpGN0NVCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQL9JCAQ1U9307UQIECAAAECBAgQIECAAAECBAgQIECAAAECBGokIBBVo5uhKgQIECBAgAABAgQIECBAgAABAgQIECBAgACBfhIQiOqnu+laCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQI1EhCIqtHNUBUCBAgQIECAAAECBAgQIECAAAECBAgQIECAQD8JCET10910LQQIECBAgAABAgQIECBAgAABAgQIECBAgACBGgkIRNXoZqgKAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKCfBASi+uluuhYCBAgQIECAAAECBAgQIECAAAECBAgQIECAQI0EBKJqdDNUhQABAgQIECBAgAABAgQIECBAgAABAgQIECDQTwICUf10N10LAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKBGAgJRNboZqkKAAAECBAgQIECAAAECBAgQIECAAAECBAgQ6CcBgah+upuuhQABAgQIECBAgAABAgQIECBAgAABAgQIECBQIwGBqBrdDFUhQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECPSTgEBUP91N10KAAAECBAgQIECAAAECBAgQIECAAAECBAgQqJGAQFSNboaqECBAgAABAgQIECBAgAABAgQIECBAgAABAgT6SUAgqp/upmshQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECNRIQCCqRjdDVQgQIECAAAECBAgQIECAAAECBAgQIECAAAEC/SQgENVPd9O1ECBAgAABAgQIECBAgAABAgQIECBAgAABAgRqJCAQVaOboSoECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgX4SEIjqp7vpWggQIECAAAECBAgQIECAAAECBAgQIECAAAECNRIQiKrRzVAVAgQIECBAgAABAgQIECBAgAABAgQIECBAgEA/CQhE9dPddC0ECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgRoJCETV6GaoCgECBAgQIECAAAECBAgQIECAAAECBAgQIECgnwQEovrpbroWAgQIECBAgAABAgQIECBAgAABAgQIECBAgECNBASianQzVIUAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0E8CAlH9dDddCwECBAgQIECAAAECBAgQIECAAAECBAgQIECgRgICUTW6GapCgAABAgQIECBAgAABAgQIECBAgAABAgQIEOgnAYGofrqbroUAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUCMBgaga3QxVIUCAAAECBAgQIECAAAECBAgQIECAAAECBAj0k4BAVD/dTddCgAABAgQIECBAgAABAgQIECBAgAABAgQIEKiRgEBUjW6GqhAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIE+klAIKqf7qZrIUCAAAECBAgQIECAAAECBAgQIECAAAECBAjUSEAgqkY3Q1UIECBAgAABAgQIECBAgAABAgQIECBAgAABAv0kIBDVT3fTtRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEaiQgEFWjm6EqBAgQIECAAAECBAgQIECAAAECBAgQIECAAIF+EhCI6qe76VoIECBAgAABAgQIECBAgAABAgQIECBAgAABAjUSEIiq0c1QFQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAPwkIRPXT3XQtBAgQIECAAAECBAgQIECAAAECBAgQIECAAIEaCQhE1ehmqAoBAgQIECBAgAABAgQIECBAgAABAgQIECBAoJ8EBKL66W66FgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAjQQEomp0M1SFAAECBAgQIECAAAECBAgQIECAAAECBAgQINBPAgJR/XQ3XQsBAgQIECBAgAABAgQIECBAgAABAgQIECBAoEYCAlE1uhmqQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBDoJwGBqH66m66FAAECBAgQIECAAAECBAgQIECAAAECBAgQIFAjAYGoGt0MVSFAgAABAgQIECBAgAABAgQIECBAgAABAgQI9JOAQFQ/3U3XQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCokYBAVI1uhqoQIECAAAECBAgQIECAAAECBAgQIECAAAECBPpJQCCqn+6mayFAgAABAgQIECBAgAABAgQIECBAgAABAgQI1EhAIKpGN0NVCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQL9JCAQ1U9307UQIECAAAECBAgQIECAAAECBAgQIECAAAECBGokIBBVo5uhKgQIECBAgAABAgQIECBAgAABAgQIECBAgACBfhIQiOqnu+laCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQI1EhCIqtHNUBUCBAgQIECAAAECBAgQIECAAAECBAgQIECAQD8JCET10910LQQIECBAgAABAgQIECBAgAABAgQIECBAgACBGgn8PwtU7bM/rCRbAAAAAElFTkSuQmCC"
+ }
+ },
+ "cell_type": "markdown",
+ "id": "8889a307-fa3f-4d38-9127-d41e4686ae47",
+ "metadata": {},
+ "source": [
+ "# Corrective RAG (CRAG)\n",
+ "\n",
+ "Corrective-RAG (CRAG) is a strategy for RAG that incorporates self-reflection / self-grading on retrieved documents. \n",
+ "\n",
+ "In the paper [here](https://arxiv.org/pdf/2401.15884.pdf), a few steps are taken:\n",
+ "\n",
+ "* If at least one document exceeds the threshold for relevance, then it proceeds to generation\n",
+ "* Before generation, it performs knowledge refinement\n",
+ "* This partitions the document into \"knowledge strips\"\n",
+ "* It grades each strip, and filters our irrelevant ones\n",
+ "* If all documents fall below the relevance threshold or if the grader is unsure, then the framework seeks an additional datasource\n",
+ "* It will use web search to supplement retrieval\n",
+ " \n",
+ "We will implement some of these ideas from scratch using [LangGraph](https://langchain-ai.github.io/langgraph/):\n",
+ "\n",
+ "* Let's skip the knowledge refinement phase as a first pass. This can be added back as a node, if desired. \n",
+ "* If *any* documents are irrelevant, let's opt to supplement retrieval with web search. \n",
+ "* We'll use [Tavily Search](https://python.langchain.com/v0.2/docs/integrations/tools/tavily_search/) for web search.\n",
+ "* Let's use query re-writing to optimize the query for web search.\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4931ac25-99f9-4f04-b3d1-4683f7853667",
+ "metadata": {},
+ "source": [
+ "## Setup\n",
+ "\n",
+ "First, let's download our required packages and set our API keys"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "568c84d6-9df6-4b7b-b50d-476c0a64a04b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "! pip install langchain_community tiktoken langchain-openai langchainhub chromadb langchain langgraph tavily-python"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "74710419-158d-4270-931c-de83db7b580d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import getpass\n",
+ "import os\n",
+ "\n",
+ "\n",
+ "def _set_env(key: str):\n",
+ " if key not in os.environ:\n",
+ " os.environ[key] = getpass.getpass(f\"{key}:\")\n",
+ "\n",
+ "\n",
+ "_set_env(\"OPENAI_API_KEY\")\n",
+ "_set_env(\"TAVILY_API_KEY\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3adde047",
+ "metadata": {},
+ "source": [
+ "
\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": "a21f32d2-92ce-4995-b309-99347bafe3be",
+ "metadata": {},
+ "source": [
+ "## Create Index\n",
+ " \n",
+ "Let's index 3 blog posts."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "3a566a30-cf0e-4330-ad4d-9bf994bdfa86",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
+ "from langchain_community.document_loaders import WebBaseLoader\n",
+ "from langchain_community.vectorstores import Chroma\n",
+ "from langchain_openai import OpenAIEmbeddings\n",
+ "\n",
+ "urls = [\n",
+ " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
+ "]\n",
+ "\n",
+ "docs = [WebBaseLoader(url).load() for url in urls]\n",
+ "docs_list = [item for sublist in docs for item in sublist]\n",
+ "\n",
+ "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
+ " chunk_size=250, chunk_overlap=0\n",
+ ")\n",
+ "doc_splits = text_splitter.split_documents(docs_list)\n",
+ "\n",
+ "# Add to vectorDB\n",
+ "vectorstore = Chroma.from_documents(\n",
+ " documents=doc_splits,\n",
+ " collection_name=\"rag-chroma\",\n",
+ " embedding=OpenAIEmbeddings(),\n",
+ ")\n",
+ "retriever = vectorstore.as_retriever()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6fca2db8-8d68-42b0-981d-4be5ccdbe293",
+ "metadata": {},
+ "source": [
+ "## LLMs"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "7ece414c-2df5-4ffd-aa82-550a65775261",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "binary_score='yes'\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Retrieval Grader\n",
+ "\n",
+ "from langchain_core.prompts import ChatPromptTemplate\n",
+ "from langchain_core.pydantic_v1 import BaseModel, Field\n",
+ "from langchain_openai import ChatOpenAI\n",
+ "\n",
+ "\n",
+ "# Data model\n",
+ "class GradeDocuments(BaseModel):\n",
+ " \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n",
+ "\n",
+ " binary_score: str = Field(\n",
+ " description=\"Documents are relevant to the question, 'yes' or 'no'\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# LLM with function call\n",
+ "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
+ "structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
+ "\n",
+ "# Prompt\n",
+ "system = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
+ " If the document contains keyword(s) or semantic meaning related to the question, grade it as relevant. \\n\n",
+ " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n",
+ "grade_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"system\", system),\n",
+ " (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "retrieval_grader = grade_prompt | structured_llm_grader\n",
+ "question = \"agent memory\"\n",
+ "docs = retriever.get_relevant_documents(question)\n",
+ "doc_txt = docs[1].page_content\n",
+ "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "a207c85f-e414-46b7-8999-4c0ead1493da",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The design of generative agents combines LLM with memory, planning, and reflection mechanisms to enable agents to behave conditioned on past experience. Memory stream is a long-term memory module that records a comprehensive list of agents' experience in natural language. Short-term memory is utilized for in-context learning, while long-term memory allows agents to retain and recall information over extended periods.\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Generate\n",
+ "\n",
+ "from langchain import hub\n",
+ "from langchain_core.output_parsers import StrOutputParser\n",
+ "\n",
+ "# Prompt\n",
+ "prompt = hub.pull(\"rlm/rag-prompt\")\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n",
+ "\n",
+ "\n",
+ "# Post-processing\n",
+ "def format_docs(docs):\n",
+ " return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
+ "\n",
+ "\n",
+ "# Chain\n",
+ "rag_chain = prompt | llm | StrOutputParser()\n",
+ "\n",
+ "# Run\n",
+ "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
+ "print(generation)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "30d0a69b-9087-4f85-af26-cab55b567872",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'What is the role of memory in artificial intelligence agents?'"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Question Re-writer\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
+ "\n",
+ "# Prompt\n",
+ "system = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n",
+ " for web search. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\n",
+ "re_write_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"system\", system),\n",
+ " (\n",
+ " \"human\",\n",
+ " \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n",
+ " ),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "question_rewriter = re_write_prompt | llm | StrOutputParser()\n",
+ "question_rewriter.invoke({\"question\": question})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e4538467-4a15-4733-b93c-2b79d4d6bf25",
+ "metadata": {},
+ "source": [
+ "## Web Search Tool"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 38,
+ "id": "46d51b53-54a9-4e0a-9f14-e39998f5b340",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "### Search\n",
+ "\n",
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "\n",
+ "web_search_tool = TavilySearchResults(k=3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "87194a1b-535a-4593-ab95-5736fae176d1",
+ "metadata": {},
+ "source": [
+ "## Create Graph \n",
+ "\n",
+ "Now let's create our graph that will use CRAG\n",
+ "\n",
+ "### Define Graph State"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 39,
+ "id": "94b3945f-ef0f-458d-a443-f763903550b0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from typing import List\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "\n",
+ "class GraphState(TypedDict):\n",
+ " \"\"\"\n",
+ " Represents the state of our graph.\n",
+ "\n",
+ " Attributes:\n",
+ " question: question\n",
+ " generation: LLM generation\n",
+ " web_search: whether to add search\n",
+ " documents: list of documents\n",
+ " \"\"\"\n",
+ "\n",
+ " question: str\n",
+ " generation: str\n",
+ " web_search: str\n",
+ " documents: List[str]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 40,
+ "id": "efd639c5-82e2-45e6-a94a-6a4039646ef5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langchain.schema import Document\n",
+ "\n",
+ "\n",
+ "def retrieve(state):\n",
+ " \"\"\"\n",
+ " Retrieve documents\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, documents, that contains retrieved documents\n",
+ " \"\"\"\n",
+ " print(\"---RETRIEVE---\")\n",
+ " question = state[\"question\"]\n",
+ "\n",
+ " # Retrieval\n",
+ " documents = retriever.get_relevant_documents(question)\n",
+ " return {\"documents\": documents, \"question\": question}\n",
+ "\n",
+ "\n",
+ "def generate(state):\n",
+ " \"\"\"\n",
+ " Generate answer\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, generation, that contains LLM generation\n",
+ " \"\"\"\n",
+ " print(\"---GENERATE---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # RAG generation\n",
+ " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
+ " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
+ "\n",
+ "\n",
+ "def grade_documents(state):\n",
+ " \"\"\"\n",
+ " Determines whether the retrieved documents are relevant to the question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates documents key with only filtered relevant documents\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # Score each doc\n",
+ " filtered_docs = []\n",
+ " web_search = \"No\"\n",
+ " for d in documents:\n",
+ " score = retrieval_grader.invoke(\n",
+ " {\"question\": question, \"document\": d.page_content}\n",
+ " )\n",
+ " grade = score.binary_score\n",
+ " if grade == \"yes\":\n",
+ " print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
+ " filtered_docs.append(d)\n",
+ " else:\n",
+ " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n",
+ " web_search = \"Yes\"\n",
+ " continue\n",
+ " return {\"documents\": filtered_docs, \"question\": question, \"web_search\": web_search}\n",
+ "\n",
+ "\n",
+ "def transform_query(state):\n",
+ " \"\"\"\n",
+ " Transform the query to produce a better question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates question key with a re-phrased question\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---TRANSFORM QUERY---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # Re-write question\n",
+ " better_question = question_rewriter.invoke({\"question\": question})\n",
+ " return {\"documents\": documents, \"question\": better_question}\n",
+ "\n",
+ "\n",
+ "def web_search(state):\n",
+ " \"\"\"\n",
+ " Web search based on the re-phrased question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates documents key with appended web results\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---WEB SEARCH---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # Web search\n",
+ " docs = web_search_tool.invoke({\"query\": question})\n",
+ " web_results = \"\\n\".join([d[\"content\"] for d in docs])\n",
+ " web_results = Document(page_content=web_results)\n",
+ " documents.append(web_results)\n",
+ "\n",
+ " return {\"documents\": documents, \"question\": question}\n",
+ "\n",
+ "\n",
+ "### Edges\n",
+ "\n",
+ "\n",
+ "def decide_to_generate(state):\n",
+ " \"\"\"\n",
+ " Determines whether to generate an answer, or re-generate a question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Binary decision for next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---ASSESS GRADED DOCUMENTS---\")\n",
+ " state[\"question\"]\n",
+ " web_search = state[\"web_search\"]\n",
+ " state[\"documents\"]\n",
+ "\n",
+ " if web_search == \"Yes\":\n",
+ " # All documents have been filtered check_relevance\n",
+ " # We will re-generate a new query\n",
+ " print(\n",
+ " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
+ " )\n",
+ " return \"transform_query\"\n",
+ " else:\n",
+ " # We have relevant documents, so generate answer\n",
+ " print(\"---DECISION: GENERATE---\")\n",
+ " return \"generate\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fa076e90-7132-4fcf-8507-db5990314c4f",
+ "metadata": {},
+ "source": [
+ "### Compile Graph\n",
+ "\n",
+ "The just follows the flow we outlined in the figure above."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 41,
+ "id": "dedae17a-98c6-474d-90a7-9234b7c8cea0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langgraph.graph import END, StateGraph, START\n",
+ "\n",
+ "workflow = StateGraph(GraphState)\n",
+ "\n",
+ "# Define the nodes\n",
+ "workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
+ "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
+ "workflow.add_node(\"generate\", generate) # generate\n",
+ "workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
+ "workflow.add_node(\"web_search_node\", web_search) # web search\n",
+ "\n",
+ "# Build graph\n",
+ "workflow.add_edge(START, \"retrieve\")\n",
+ "workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"grade_documents\",\n",
+ " decide_to_generate,\n",
+ " {\n",
+ " \"transform_query\": \"transform_query\",\n",
+ " \"generate\": \"generate\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"transform_query\", \"web_search_node\")\n",
+ "workflow.add_edge(\"web_search_node\", \"generate\")\n",
+ "workflow.add_edge(\"generate\", END)\n",
+ "\n",
+ "# Compile\n",
+ "app = workflow.compile()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "27ba16a8",
+ "metadata": {},
+ "source": [
+ "## Use the graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 42,
+ "id": "f5b7c2fe-1fc7-4b76-bf93-ba701a40aa6b",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "---RETRIEVE---\n",
+ "\"Node 'retrieve':\"\n",
+ "'\\n---\\n'\n",
+ "---CHECK DOCUMENT RELEVANCE TO QUESTION---\n",
+ "---GRADE: DOCUMENT NOT RELEVANT---\n",
+ "---GRADE: DOCUMENT NOT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "\"Node 'grade_documents':\"\n",
+ "'\\n---\\n'\n",
+ "---ASSESS GRADED DOCUMENTS---\n",
+ "---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\n",
+ "---TRANSFORM QUERY---\n",
+ "\"Node 'transform_query':\"\n",
+ "'\\n---\\n'\n",
+ "---WEB SEARCH---\n",
+ "\"Node 'web_search_node':\"\n",
+ "'\\n---\\n'\n",
+ "---GENERATE---\n",
+ "\"Node 'generate':\"\n",
+ "'\\n---\\n'\n",
+ "\"Node '__end__':\"\n",
+ "'\\n---\\n'\n",
+ "('Agents possess short-term memory, which is utilized for in-context learning, '\n",
+ " 'and long-term memory, allowing them to retain and recall vast amounts of '\n",
+ " 'information over extended periods. Some experts also classify working memory '\n",
+ " 'as a distinct type, although it can be considered a part of short-term '\n",
+ " 'memory in many cases.')\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pprint import pprint\n",
+ "\n",
+ "# Run\n",
+ "inputs = {\"question\": \"What are the types of agent memory?\"}\n",
+ "for output in app.stream(inputs):\n",
+ " for key, value in output.items():\n",
+ " # Node\n",
+ " pprint(f\"Node '{key}':\")\n",
+ " # Optional: print full state at each node\n",
+ " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
+ " pprint(\"\\n---\\n\")\n",
+ "\n",
+ "# Final generation\n",
+ "pprint(value[\"generation\"])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 43,
+ "id": "41ea1108-f385-4774-962d-db157922e231",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "---RETRIEVE---\n",
+ "\"Node 'retrieve':\"\n",
+ "'\\n---\\n'\n",
+ "---CHECK DOCUMENT RELEVANCE TO QUESTION---\n",
+ "---GRADE: DOCUMENT NOT RELEVANT---\n",
+ "---GRADE: DOCUMENT NOT RELEVANT---\n",
+ "---GRADE: DOCUMENT NOT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "\"Node 'grade_documents':\"\n",
+ "'\\n---\\n'\n",
+ "---ASSESS GRADED DOCUMENTS---\n",
+ "---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\n",
+ "---TRANSFORM QUERY---\n",
+ "\"Node 'transform_query':\"\n",
+ "'\\n---\\n'\n",
+ "---WEB SEARCH---\n",
+ "\"Node 'web_search_node':\"\n",
+ "'\\n---\\n'\n",
+ "---GENERATE---\n",
+ "\"Node 'generate':\"\n",
+ "'\\n---\\n'\n",
+ "\"Node '__end__':\"\n",
+ "'\\n---\\n'\n",
+ "('The AlphaCodium paper functions by proposing a code-oriented iterative flow '\n",
+ " 'that involves repeatedly running and fixing generated code against '\n",
+ " 'input-output tests. Its key mechanisms include generating additional data '\n",
+ " 'like problem reflection and test reasoning to aid the iterative process, as '\n",
+ " 'well as enriching the code generation process. AlphaCodium aims to improve '\n",
+ " 'the performance of Large Language Models on code problems by following a '\n",
+ " 'test-based, multi-stage approach.')\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pprint import pprint\n",
+ "\n",
+ "# Run\n",
+ "inputs = {\"question\": \"How does the AlphaCodium paper work?\"}\n",
+ "for output in app.stream(inputs):\n",
+ " for key, value in output.items():\n",
+ " # Node\n",
+ " pprint(f\"Node '{key}':\")\n",
+ " # Optional: print full state at each node\n",
+ " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
+ " pprint(\"\\n---\\n\")\n",
+ "\n",
+ "# Final generation\n",
+ "pprint(value[\"generation\"])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a7e44593-1959-4abf-8405-5e23aa9398f5",
+ "metadata": {},
+ "source": [
+ "LangSmith Traces - \n",
+ " \n",
+ "* https://smith.langchain.com/public/f6b1716c-e842-4282-9112-1026b93e246b/r\n",
+ "\n",
+ "* https://smith.langchain.com/public/497c8ed9-d9e2-429e-8ada-e64de3ec26c9/r"
+ ]
+ }
+ ],
+ "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.8"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/langgraph/source/examples/rag/langgraph_crag_local.ipynb b/langgraph/source/examples/rag/langgraph_crag_local.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..9ace47871f1e7ebb13e200bdfd5a8b8407be2bfb
--- /dev/null
+++ b/langgraph/source/examples/rag/langgraph_crag_local.ipynb
@@ -0,0 +1,861 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "ac7db067",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
+ ]
+ },
+ {
+ "attachments": {
+ "b77a7d3b-b28a-4dcf-9f1a-861f2f2c5f6c.png": {
+ "image/png": "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eGlmOlBpeGVsWERpbWVuc2lvbj45ODg8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpVc2VyQ29tbWVudD5TY3JlZW5zaG90PC9leGlmOlVzZXJDb21tZW50PgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4Kx3kmLgAAQABJREFUeAHsnQd8FVX2xw+QRgglBQiEhASSEHrvVURQQcGK2LCXtaz6l3Vd29p2XeuufcUCgiyIICBIEZDee++QAiQhvRfA//3dl3mZ9/ICBPKSl+R3/LzMzJ07d+79zpPkN+fcc2v9oUxoJEACJEACJEACJEACJEACJEACJEAC5Uqgdrm2xsZIgARIgARIgARIgARIgARIgARIgAQ0AQpufhFIgARIgARIgARIgARIgARIgARIwAkEKLidAJVNkgAJkAAJkAAJkAAJkAAJkAAJkAAFN78DJEACJEACJEACJEACJEACJEACJOAEAhTcToDKJkmABEiABEiABEiABEiABEiABEiAgpvfARIgARIgARIgARIgARIgARIgARJwAgEKbidAZZMkQAIkQAIkQAIkQAIkQAIkQAIkQMHN7wAJkAAJkAAJkAAJkAAJkAAJkAAJOIEABbcToLJJEiABEiABEiABEiABEiABEiABEqDg5neABEiABEiABEiABEiABEiABEiABJxAgILbCVDZJAmQAAmQAAmQAAmQAAmQAAmQAAlQcPM7QAIkQAIkQAIkQAIkQAIkQAIkQAJOIEDB7QSobJIESIAESIAESIAESIAESIAESIAEKLj5HSABEiABEiABEiABEiABEiABEiABJxCg4HYCVDZJAiRAAiRAAiRAAiRAAiRAAiRAAhTc/A6QAAmQAAmQAAmQAAmQAAmQAAmQgBMIUHA7ASqbJAESIAESIAESIAESIAESIAESIAEKbn4HSIAESIAESIAESIAESIAESIAESMAJBCi4nQCVTZIACZAACZAACZAACZAACZAACZAABTe/AyRAAiRAAiRAAiRAAiRAAiRAAiTgBAIU3E6AyiZJgARIgARIgARIgARIgARIgARIgIKb3wESIAESIAESIAESIAESIAESIAEScAIBCm4nQGWTJEACJEACJEACJEACJEACJEACJEDBze8ACZAACZAACZAACZAACZAACZAACTiBAAW3E6CySRIgARIgARIgARIgARIgARIgARKg4OZ3gARIgARIgARIgARIgARIgARIgAScQICC2wlQ2SQJkAAJkAAJkAAJkAAJkAAJkAAJUHDzO0ACJEACJEACJEACJEACJEACJEACTiBAwe0EqGySBEiABEiABEiABEiABEiABEiABCi4+R0gARIgARIgARIgARIgARIgARIgAScQoOB2AlQ2SQIkQAIkQAIkQAIkQAIkQAIkQAIU3PwOkAAJkAAJkAAJkAAJkAAJkAAJkIATCFBwOwEqmyQBEiABEiABEiABEiABEiABEiABCm5+B0iABEiABEiABEiABEiABEiABEjACQQouJ0AlU2SAAmQAAmQAAmQAAmQAAmQAAmQAAU3vwMkQAIkQAIkQAIkQAIkQAIkQAIk4AQCFNxOgMomSYAESIAESIAESIAESIAESIAESICCm98BEiABEiABEiABEiABEiABEiABEnACAQpuJ0BlkyRAAiRAAiRAAiRAAiRAAiRAAiRAwc3vAAmQAAmQAAmQAAmQAAmQAAmQAAk4gQAFtxOgskkSIAESIAESIAESIAESIAESIAESoODmd4AESIAESIAESIAESIAESIAESIAEnECAgtsJUNkkCZAACZAACZAACZAACZAACZAACVBw8ztAAiRAAiRAAiRAAiRAAiRAAiRAAk4gQMHtBKhskgRIgARIgARIgARIgARIgARIgAQouPkdIAESIAESIAESIAESIAESIAESIAEnEKDgdgJUNkkCJEACJEACJEACJEACJEACJEACFNz8DpAACZAACZAACZAACZAACZAACZCAEwhQcDsBKpskARIgARIgARIgARIgARIgARIgAQpufgdIgARIgARIgARIgARIgARIgARIwAkEKLidAJVNkgAJkAAJkAAJkAAJkAAJkAAJkAAFN78DJEACJEACJEACJEACJEACJEACJOAEAhTcToDKJkmABEiABEiABEiABEiABEiABEiAgpvfARIgARIgARIgARIgARIgARIgARJwAgEKbidAZZMkQAIkQAIkQAIkQAIkQAIkQAIkQMHN7wAJkAAJkAAJkAAJkAAJkAAJkAAJOIEABbcToLJJEiABEiABEiABEiABEiABEiABEqDg5neABEiABEiABEiABEiABEiABEiABJxAgILbCVDZJAmQAAmQAAmQAAmQAAmQAAmQAAlQcPM7QAIkQAIkQAIkQAIkQAIkQAIkQAJOIEDB7QSobJIESIAESIAESIAESIAESIAESIAEKLj5HSABEiABEiABEiABEiABEiABEiABJxCg4HYCVDZJAiRAAiRAAiRAAiRAAiRAAiRAAhTc/A6QAAmQAAmQAAmQAAmQAAmQAAmQgBMIUHA7ASqbJAESIAESIAESIAESIAESIAESIAEKbn4HSIAESIAESIAESIAESIAESIAESMAJBCi4nQCVTZIACZAACZAACZAACZAACZAACZAABTe/AyRAAiRAAiRAAiRAAiRAAiRAAiTgBAIU3E6AyiZJgARIgARIgARIgARIgARIgARIgIKb3wESIAESIAESIAESIAESIAESIAEScAIBCm4nQGWTJEACJEACJEACJEACJEACJEACJEDBze8ACZAACZAACZAACZAACZAACZAACTiBAAW3E6CySRIgARIgARIgARIgARIgARIgARKg4OZ3gARIgARIgARIgARIgARIgARIgAScQICC2wlQ2SQJkAAJkAAJkAAJkAAJkAAJkAAJUHDzO0ACJEACJEACJEACJEACJEACJEACTiBAwe0EqGySBEiABEiABEiABEiABEiABEiABCi4+R0gARIgARIgARIgARIgARIgARIgAScQoOB2AlQ2SQIkQAIkQAIkQAIkQAIkQAIkQAIU3PwOkAAJkAAJkAAJkAAJkAAJkAAJkIATCFBwOwEqmyQBEiABEiABEiABEiABEiABEiABCm5+B0iABEiABEiABEiABEiABEiABEjACQQouJ0AlU2SAAmQAAmQAAmQAAmQAAmQAAmQAAU3vwMkQAIkQAIkQAIkQAIkQAIkQAIk4AQCFNxOgMomSYAESIAESIAESIAESIAESIAESICCm98BEiABEiABEiABEiABEiABEiABEnACAQpuJ0BlkyRAAiRAAiRAAiRAAiRAAiRAAiRAwc3vAAmQAAmQAAmQAAmQAAmQAAmQAAk4gYCbE9pkkyRAAiRAAiRAAiRAAjWcwNnzZyW7IKcEBY867lLXvW6JckcF9m3U9/SR2rUuzV+UW5grx1JOCNpo7R8mPh4+jm7BMhIgARJwKgEKbqfiZeMkQAIkQAIkQAIkUDMJrDmxTj75/UOHg3dToru5b4g81Ochad+0rcM6KJy67X/yy85Z1vOPDXparokYaj12tDNrz88yZ8csycnPsjnt4e4lozqMlru6jbMp5wEJkAAJOJPApb0idGYP2DYJkAAJkAAJkAAJkECNInD2XKHEJB2VV+e/KPP3Lyx17JtOrLc5tzF6g82x+SAzP1NemP9XmbZxcgmxjXoFhXkye/sMeX3Jm+bLuE8CJEACTiVAD7dT8bJxEiABEiABEiABEiCBW7uPk+BGwRpEbFqsLD2wRNKyk/Xx5A0TpV9oH/Gr62sDCgI6If2ULvPxaiBZeRmyO267/KH+q6X+s7cv1n8lRxIO6OLatWvL3b3vl45NO4iXh5dsid0qUzZ+K+fPn5duwd3sL+UxCZAACTiNAAW309CyYRIgARIgARIgARIgARDoG9JHQv1aWmGM6zJWXvr1ZTlweo8WwbvVdnCrgdbz2NkQs8l6/GC/R+Q/y98XeMYPJB6Stk3aWM9hJzY9TjYeXa3LvDy85cOb/i1NfZpY69zYbpS6JkpOpMZcNCTdehF3SIAESKAcCDCkvBwgsgkSIAESIAESIAESIIGyEbgm6hrrBSdSoq37xs76onDyRvX8ZUBoP4HXGrbOLswcZf9ToeKG3dPrPhuxbZRHBIRTbBswuCUBEqgwAhTcFYaaNyIBEiABEiABEiABEjAIuNV2N3bF19s2nPz8H3/I3pM79fmuwT10ZvLwouRqm6Jt53WjUqzKRg5DMrbhkcP0Pn+QAAmQgCsQoOB2hafAPpAACZAACZAACZBADSOwcN+v1hF3ad7Zuo+dA4kHdPg49nuF9MRGehZtkzISJC0vXZcZP5KyzujdgPpNL3nZMONabkmABEjAmQQ4h9uZdNk2CZAACZAACZAACZCALD/yuwT4NNYkTqWflLVHV1kzicMrHdKohQ2ldaZs5J2bddTneipP9w8bJ+n9DdEb5do2w/V+oZrXjQzksCZKcJvteJHn21xW36u+BHj7m4u4TwIkQAJOI0DB7TS0bJgESIAESIAESIAESAAEFuye4xAExPaLw18pcW5zkeAO9g8TTzdPfT64YQuVcdxb8gpyBPO7DcFdq1ZxxvKCcwXWtpDN/Pmfn7EeGzs9w/rLX4dOMA65JQESIAGnEqDgdipeNk4CJEACJEACJEACJFAagffGfFTCu52amyYIG4fBK77r9F7r5Y28/SReCe59p3bK+T/O6/Bxt9pu4uHupb3cSVmJ1rrcIQESIAFXIEDB7QpPgX0gARIgARIgARIggWpM4IOb/mNdFmzajukya+t0Pdq10euU4L7dZuTm5cC2R28SfOwN62nvTdgvHQPb61N+9QIkPi1OUtRcbni5Pep46LW637rhX9ZL31v+L0nPTrEec4cESIAEKoIAk6ZVBGXegwRIgARIgARIgARIQBO4qf1o6xJfc7bPlLyzlvnXBp4NDpb9Ms6Zt+blwVr4ButTEOLz9xcnY8N63cbHo44lNN3cBvdJgARIwNkEKLidTZjtkwAJkAAJkAAJkAAJWAnUda8rI9qN0sdnVcKzWbt+tp5DmDjCxWERgW3l37d+WuKDdblhm0zCfGznYi/5zG3T5VjKcV2HP0iABEigsglQcFf2E+D9SYAESIAESIAESKCGEbit863WEc/bNVtyC3P18Z74fQIvNaxf2ABBojT7T4+QXvp8WnayJOdYQsRbqeRqHYO76XJkLJ/w87PyzaZJsjFms5zKPC2HzhyW3IJsfZ4/SIAESKAiCVBwVyRt3osESIAESIAESIAESEAaejWQIUXLesHL/ZMS3bB1J9ZZ6fQuWnfbWlC006tIcONwvVoezLCnBz4lIQGtjUP5VWVGf/e3t+WpHx+XF+dNkKy8DOs57pAACZBARRGg4K4o0rwPCZAACZAACZAACdQgAsgebljt2nWMXet2XLex1v15u2bJufPnZEfcdl3m7ekjTX2aWM+bdzo2syRKQ9nW2C3WU351feXD0e/LuF7jxQg7t54s2mng3Ug6Nu9gX8xjEiABEnAagVp/KHNa62yYBEiABEiABEiABEiABCqBwHn1J258Zrxk5WeJXz0/8fVqJHUcCP9K6BpvSQIkUIMIUHDXoIfNoZIACZAACZAACZAACZAACZAACVQcAYaUVxxr3okESIAESIAESIAESIAESIAESKAGEaDgrkEPm0MlARIgARIgARIgARIgARIgARKoOAIU3BXHmnciARIgARIgARIgARIgARIgARKoQQQouGvQw+ZQSYAESIAESIAESIAESIAESIAEKo4ABXfFseadSIAESIAESIAESIAESIAESIAEahABCu4a9LA5VBIgARIgARIgARIgARIgARIggYojQMFdcax5JxIgARIgARIgARIgARIgARIggRpEgIK7Bj1sDpUESIAESIAESIAESIAESIAESKDiCFBwVxxr3okESIAESIAESIAESIAESIAESKAGEaDgrkEPm0MlARIgARIgARIgARIgARIgARKoOAIU3BXHmnciARIgARIgARIgARIgARIgARKoQQQouGvQw+ZQSYAESIAESIAESKAqEDiafEymbp0m6XkZVaG77CMJkAAJlEqg1h/KSj3LEyRAAiRAAiRAAiRAAiRQgQS+3TpJFuyYo+/YvHGwtGjSQuLOxMqpxLgSvWjROFS6hXSTns17SrsmbUucZwEJkAAJVDYBCu7KfgK8PwmQAAmQAAmQAAnUcAJb4rbJ5rgtcvDMfolNPH5RGu5u7tIhvLP41K0v2w5ukeycTPFrECA9w/rIuI5jpb5n/Yu2wQokQAIkUBEEKLgrgjLvQQIkQAIkQAIkQAIkUIJAdGqM/Lxnrqw+tEwiAttJwR95kpmfIR5uHhLUJFhqqf9geQW5kp2bLdl52ZKDT26WnD9/vkR7KGhU30+ujhohd3S8TWrX4uxJh5BYSAIkUGEEKLgrDDVvRAIkQAIkQAIkQAIkYBCYuu1/8uueeeLvEyCD2w6VA0n7ZPuhzdIpsqsM7jTEqFbqNiMnQ5IzUiQlM1l9UiRV7adlpkp+fq6+JtAvSEa2vUGuj7q21DZ4ggRIgAScTYCC29mE2T4JkAAJkAAJkAAJkIANgSXKo/3f1Z/I9Z1vlLZhbeW7Fd9IQWGB9O3QX6KCo2zqXs7BnhO7ZcOe9ZKrvOEt/FvKLZ1vk0FhAy6nKV5DAiRAAldEgIL7ivDxYhIgARIgARIgARIggbIQ2Bq3Xf6x+HV5+Oo/qXnbG2XHwa3SKjhSBiix3bBeo7I0dcG6CENftm2ZHIs7rOtNuOZv0iek1wWv4UkSIAESKG8CnNhS3kTZHgmQAAmQAAmQAAmQgEMCx1NOyJdrP5e373xHlh5YosV2744DZGTvkeUqtnFzL4+6MrLPKAluFqr7EpMVo7f8QQIkQAIVSYAe7oqkzXuRAAmQAAmQAAmQQA0m8NnaLyQ644Rk5KXJmZQEubbvKIkIinA6kRm/T5c6tWvLhBEvSEvPEKffjzcgARIgAYMAPdwGCW5JgARIgARIgARIgAScRiAmNVZWHl4moUGhWmyPGXxrhYhtDGhkn5GSqAT+rD2zJKkw2WljZMMkQAIkYE+AgtueCI9JgARIgARIgARIgATKncCCgwvFx9tHlm1eLD3a9ZHgxsHlfo/SGsR63d3b9pK1O1bK1jNb5Q/1H831CCxbtkxeffVViYlh+L/rPR326HIJUHBfLjleRwIkQAIkQAIkQAIkcEkEErPOyJojKyQ9M03ate4ofdv1vaTryrNS76jeEhnaTmZv+ElO5J0oz6Zdsq3s7GyZMmWKxMXFuWT/7DsVGxsrDzzwgEyePFl++OEH+9M8JoEqS4CCu8o+OnacBEiABEiABEiABKoGgbn750lefo54enhJt4juldbpXlG9JCk1UaZs+qHae7l//vln+eSTT6R///4yduxY+eijj+To0aOVxv5iN/7999+tVby8vKz73CGBqk6AgruqP0H2nwRIgARIgARIgARcnMCBxP26h50iuoqvj2+l9Rb3DmkWJjsPb5FVcWsqrR8VceO7775bZs+eLS+//LKcO3dO/v3vf8vQoUNlzJgx8q9//UtWrFgh+fn5FdGVS7rH999/b63n61t53xFrJ7hDAuVEgIK7nECyGRIgARIgARIgARIggZIEUvJTJTrhqDRq4CfdI7qVrFDBJV2L+rD2ePUW3MDaokULefjhh+Wnn36SX375RSZMmCCenp7y+eefy/jx47X3+6mnnpKpU6dWqvd7+/btcviwZb109Lthw4bY0EigWhBwqxaj4CBqPIGzZ8+qULUCzaGuCkOqU8fxu6RCVS+/qJ5PPe8az40ASIAESIAESMDZBDae3CR//PGHdGzdSdzdPJx9u4u2H9IkRHu598fuleyCbKnnUe+i11SHCp06dRJ8nnzySTly5IisXr1aEMY9b948/cEYu3btKsOGDZMRI0ZIRITzl2szuML7brb69eubD7lPAlWaAAV3lX587LxBYP/BI7JgyXJ92K93DxnUr5dxymY7Z/5iOXo8Wpc9ct+d4ufbyOZ8RR1MnTFb3yqsZYj079Ojom7r1PssWrpCkpJTxNvbW26+4Vqn3ouNkwAJkAAJVB0Cu07t0p0Nbx7uMp3u2KqjLFg7T5Yd/11ubDPKZfpVUR0JDw8XfO6//345dOiQIDs4Pps3bxZ4m9977z255ppr9Afiu1Ej5/29BMGP8Haz+fj4mA8vaz8hIUG2bNmiXy4kJydL37595brrrrustswX4eVRrVq1zEVl3s/KytJt1KtXM172lBlQNbuAgruaPdCaOpzI8FZW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HHSfeuqpVlxc7DbbYYcdbMSIEfbSSy/Z559/nmhX1iVZwAfc1HAnGZbsEEAgZQIE3CmjJWMEEEAAAQQQyBSBN954w04//XTbZptt7KqrrrJRo0ZVKHo46NZ2S5Ysce+rWbnS008/7Z75J7UCvmbbP6f2aOSOAAIIbL4AAffmG5IDAggggAACCGS4wN/+9jd74IEHXMD98MMP20knnWRnnnmmffjhh7EzCwfdem/hwoV2yCGH2IABA1yz8m+//Ta2LQupEaCGOzWu5IoAAqkTaHR1kFKXPTkjgAACCCCAAAKZIbDFFlu4AHrIkCGur7YGRHv++edt2rRp1rFjR+vcubPtuOOO1qBBA3vuueds7Nixtscee5iCwP/9739WVFRku+66a2acbIaW8oUXXrCpU6fascceax06dMjQs6DYCCCQSwIE3Ll0tTlXBBBAAAEEEKhWwAfeQ4cOdcH0f/7zH3vqqads3rx5LvBWrbaCbgXjX3zxhWkEc/UB//HHH+3www9304VVe5DQBqpFf+2116xZs2bWrl270Dvm8tRxVq9ebT169KjwXi6+8DdAjjrqKOvUqVMuEnDOCCCQYQI0Kc+wC0ZxEUAAAQQQQKB+BDQg2l133WUvv/yynXLKKS6oPvDAA+2CCy6wbbfd1j2PGTPGrrnmGtt7771t7ty59uKLL9a6cE2bNrXrr7/e9R2P3/nGG2907zVq1Cj+rZx87ftu++ecROCkEUAgowQIuDPqclFYBBBAAAEEEKhvga233trUA0+10BdffLF99dVXdsIJJ7iRyffbbz/XtHzcuHGuWBqxvLZp2LBhtu+++9rHH39cYbTzCRMm2FtvveWaqaspO8ls0KBBjoFRyvlrQACBTBFosD5ImVJYyokAAggggAACCEQtUFpa6mq7Vfs9adIka9OmjS1atMg1Cdc83aNHj7ZevXrVqpgacG3//fd3fcL/9a9/uX3POusse/XVV12tuUZP90nH0jbffPON5efnm6YnU5/mxo0b+01cE3SNvK7m6hrcbfny5da6dWvbfffdTc2xMzmdc845dvfdd2fyKVB2BBDIIQEC7hy62JwqAggggAACCCRX4L333nN9ucNNyc877zy78MILa32g3//+96b+4qolLygocLXeaqqu0dN9UrP1X/ziFy6I9uv0HN5Og7idccYZrnbcb6P+4boZoGD1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Yy332MeabuhXXklRWY0AAggkTYCAO2mUZIQAAggggAACCCBQmUDHI39tLXba1RaOfsdKi8un5aps21SsX/DW69awSYF1PumUVGRPnggggEBCAQLuhCysRAABBBBAAAEEEEi2QK/L/mzry0ptzgvPJTvrKvOb+dQTwaBtX1nHU35jhT16VrktbyKAAALJFCDgTqYmeSGAAAIIIIAAAghUKtCoaVPre/vdLvid89KLlW6XzDfmvPRvWzb2U2vx812s468OT2bW5IUAAghUK0DAXS0RGyCAAAIIIIAAAggkS6Bo4EBrd8xJtvh/o2zqP++x0uXFycq6Qj7rSkpszosvBMcZba33PsD63HRLhfd5gQACCNSHQIP1QaqPA3EMBBBAAAEEEEAAAQS8wKzHHrG5D91reS1bW/sDD7WWWw/1b2328/LvJtr8116x1bOmBX22z7BOJ5682XmSAQIIIFAXAQLuuqixDwIIIIAAAggggMBmC8y47x6b//SjLp9mWw6yVjvsZC22GlLnfEvmz7elYz6zhe++YfntO1n7o46zDof9qs75sSMCCCCwuQIE3JsryP4IIIAAAggggAACdRaY99+Xbd6/HrDShfNdHj7wbta3nzUqKKg233WlpbZq+jRb/PGHtvzrL61hYVNrtec+1vX0M6xR8xbV7s8GCCCAQCoFCLhTqUveCCCAAAIIIIAAAtUKrJ49y2Y/9IAtefu12LYNGudZQdfuVtiztzXrP8DWrV5ta1evCp5L3HPpwoVWMnO6lcyd5fbJb9vemu8QDIx29DHWpGu3WD4sIIAAAlEKEHBHqc+xEUAAAQQQQAABBGICqu1e9snHtnrC11a6ZGFsfWULBd16WmG//tZy5+HWevc9zBo1qmxT1iOAAAKRCBBwR8LOQRFAAAEEEEAAAQSqElgxaZIVj/3C1syba2tXrLC1wWjmes5v29aab/sza77NttakU+eqsuA9BBBAIHIBAu7ILwEFQAABBBBAAAEEEEAAAQQQyEYB5uHOxqvKOSGAAAIIIIAAAggggAACCEQuQMAd+SWgAAgggAACCCCAAAIIIIAAAtkoQMCdjVeVc0IAAQQQQAABBBBAAAEEEIhcgIA78ktAARBAAAEEEEAAAQQQQAABBLJRgIA7G68q54QAAggggAACCCCAAAIIIBC5AAF35JeAAiCAAAIIIIAAAggg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"
+ }
+ },
+ "cell_type": "markdown",
+ "id": "92ddc4f4-f7bf-4e0e-b5a5-5abd8a008b21",
+ "metadata": {},
+ "source": [
+ "# Corrective RAG (CRAG) using local LLMs\n",
+ "\n",
+ "[Corrective-RAG (CRAG)](https://arxiv.org/abs/2401.15884) is a strategy for RAG that incorporates self-reflection / self-grading on retrieved documents. \n",
+ "\n",
+ "The paper follows this general flow:\n",
+ "\n",
+ "* If at least one document exceeds the threshold for `relevance`, then it proceeds to generation\n",
+ "* If all documents fall below the `relevance` threshold or if the grader is unsure, then it uses web search to supplement retrieval\n",
+ "* Before generation, it performs knowledge refinement of the search or retrieved documents\n",
+ "* This partitions the document into `knowledge strips`\n",
+ "* It grades each strip, and filters out irrelevant ones\n",
+ "\n",
+ "We will implement some of these ideas from scratch using [LangGraph](https://langchain-ai.github.io/langgraph/):\n",
+ "\n",
+ "* If *any* documents are irrelevant, we'll supplement retrieval with web search. \n",
+ "* We'll skip the knowledge refinement, but this can be added back as a node if desired. \n",
+ "* We'll use [Tavily Search](https://python.langchain.com/v0.2/docs/integrations/tools/tavily_search/) for web search.\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6ba4302f-09d9-4d2a-a18d-a6fd23704850",
+ "metadata": {},
+ "source": [
+ "## Setup\n",
+ "\n",
+ "We'll use [Ollama](https://ollama.ai/) to access a local LLM:\n",
+ "\n",
+ "* Download [Ollama app](https://ollama.ai/).\n",
+ "* Pull your model of choice, e.g.: `ollama pull llama3`\n",
+ "\n",
+ "We'll use [Tavily](https://python.langchain.com/v0.2/docs/integrations/tools/tavily_search/) for web search.\n",
+ "\n",
+ "We'll use a vectorstore with [Nomic local embeddings](https://blog.nomic.ai/posts/nomic-embed-text-v1) or, optionally, OpenAI embeddings.\n",
+ "\n",
+ "\n",
+ "Let's install our required packages and set our API keys:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4a660963-bd3d-4c87-b2e4-b6e432055211",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install -U langchain_community tiktoken langchainhub scikit-learn langchain langgraph tavily-python nomic[local] langchain-nomic langchain_openai"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "68316ba0-854b-41e1-9af5-1f9e965946e3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import getpass\n",
+ "import os\n",
+ "\n",
+ "\n",
+ "def _set_env(key: str):\n",
+ " if key not in os.environ:\n",
+ " os.environ[key] = getpass.getpass(f\"{key}:\")\n",
+ "\n",
+ "\n",
+ "_set_env(\"OPENAI_API_KEY\")\n",
+ "_set_env(\"TAVILY_API_KEY\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "98f863ea",
+ "metadata": {},
+ "source": [
+ "
\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": "c059c3a3-7f01-4d46-8289-fde4c1b4155f",
+ "metadata": {},
+ "source": [
+ "### LLM\n",
+ "\n",
+ "You can select from [Ollama LLMs](https://ollama.com/library)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "2f4db331-c4d0-4c7c-a9a5-0bebc8a89c6c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "local_llm = \"llama3\"\n",
+ "model_tested = \"llama3-8b\"\n",
+ "metadata = f\"CRAG, {model_tested}\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6e2b6eed-3b3f-44b5-a34a-4ade1e94caf0",
+ "metadata": {},
+ "source": [
+ "## Create Index\n",
+ "\n",
+ "Let's index 3 blog posts."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "bb8b789b-475b-4e1b-9c66-03504c837830",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "USER_AGENT environment variable not set, consider setting it to identify your requests.\n"
+ ]
+ }
+ ],
+ "source": [
+ "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
+ "from langchain_community.document_loaders import WebBaseLoader\n",
+ "from langchain_community.vectorstores import SKLearnVectorStore\n",
+ "from langchain_nomic.embeddings import NomicEmbeddings # local\n",
+ "from langchain_openai import OpenAIEmbeddings # api\n",
+ "\n",
+ "# List of URLs to load documents from\n",
+ "urls = [\n",
+ " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
+ "]\n",
+ "\n",
+ "# Load documents from the URLs\n",
+ "docs = [WebBaseLoader(url).load() for url in urls]\n",
+ "docs_list = [item for sublist in docs for item in sublist]\n",
+ "\n",
+ "# Initialize a text splitter with specified chunk size and overlap\n",
+ "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
+ " chunk_size=250, chunk_overlap=0\n",
+ ")\n",
+ "\n",
+ "# Split the documents into chunks\n",
+ "doc_splits = text_splitter.split_documents(docs_list)\n",
+ "\n",
+ "# Embedding\n",
+ "\"\"\"\n",
+ "embedding=NomicEmbeddings(\n",
+ " model=\"nomic-embed-text-v1.5\",\n",
+ " inference_mode=\"local\",\n",
+ ")\n",
+ "\"\"\"\n",
+ "embedding = OpenAIEmbeddings()\n",
+ "\n",
+ "# Add the document chunks to the \"vector store\"\n",
+ "vectorstore = SKLearnVectorStore.from_documents(\n",
+ " documents=doc_splits,\n",
+ " embedding=embedding,\n",
+ ")\n",
+ "retriever = vectorstore.as_retriever(k=4)"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "fe7fd10a-f64a-48de-a116-6d5890def1af",
+ "metadata": {},
+ "source": [
+ "## Define Tools"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "0e75c029-6c10-47c7-871c-1f4932b25309",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'score': '1'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Retrieval Grader\n",
+ "\n",
+ "from langchain.prompts import PromptTemplate\n",
+ "from langchain_community.chat_models import ChatOllama\n",
+ "from langchain_core.output_parsers import JsonOutputParser\n",
+ "from langchain_mistralai.chat_models import ChatMistralAI\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
+ "\n",
+ "# Prompt\n",
+ "prompt = PromptTemplate(\n",
+ " template=\"\"\"You are a teacher grading a quiz. You will be given: \n",
+ " 1/ a QUESTION\n",
+ " 2/ A FACT provided by the student\n",
+ " \n",
+ " You are grading RELEVANCE RECALL:\n",
+ " A score of 1 means that ANY of the statements in the FACT are relevant to the QUESTION. \n",
+ " A score of 0 means that NONE of the statements in the FACT are relevant to the QUESTION. \n",
+ " 1 is the highest (best) score. 0 is the lowest score you can give. \n",
+ " \n",
+ " Explain your reasoning in a step-by-step manner. Ensure your reasoning and conclusion are correct. \n",
+ " \n",
+ " Avoid simply stating the correct answer at the outset.\n",
+ " \n",
+ " Question: {question} \\n\n",
+ " Fact: \\n\\n {documents} \\n\\n\n",
+ " \n",
+ " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n",
+ " Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\n",
+ " \"\"\",\n",
+ " input_variables=[\"question\", \"documents\"],\n",
+ ")\n",
+ "\n",
+ "retrieval_grader = prompt | llm | JsonOutputParser()\n",
+ "question = \"agent memory\"\n",
+ "docs = retriever.invoke(question)\n",
+ "doc_txt = docs[1].page_content\n",
+ "print(retrieval_grader.invoke({\"question\": question, \"documents\": doc_txt}))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "dad03302-bd93-43fc-949e-af51a3298cfa",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The document mentions \"memory stream\" which is a long-term memory module that records a comprehensive list of agents' experience in natural language. It also discusses short-term memory and long-term memory, with the latter providing the agent with the capability to retain and recall information over extended periods. Additionally, it mentions planning and reflection mechanisms that enable agents to behave conditioned on past experience.\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Generate\n",
+ "\n",
+ "from langchain_core.output_parsers import StrOutputParser\n",
+ "\n",
+ "# Prompt\n",
+ "prompt = PromptTemplate(\n",
+ " template=\"\"\"You are an assistant for question-answering tasks. \n",
+ " \n",
+ " Use the following documents to answer the question. \n",
+ " \n",
+ " If you don't know the answer, just say that you don't know. \n",
+ " \n",
+ " Use three sentences maximum and keep the answer concise:\n",
+ " Question: {question} \n",
+ " Documents: {documents} \n",
+ " Answer: \n",
+ " \"\"\",\n",
+ " input_variables=[\"question\", \"documents\"],\n",
+ ")\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOllama(model=local_llm, temperature=0)\n",
+ "\n",
+ "# Chain\n",
+ "rag_chain = prompt | llm | StrOutputParser()\n",
+ "\n",
+ "# Run\n",
+ "generation = rag_chain.invoke({\"documents\": docs, \"question\": question})\n",
+ "print(generation)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "b36a2f36-bc5f-408d-a5e8-3fa203c233f6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "### Search\n",
+ "\n",
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "\n",
+ "web_search_tool = TavilySearchResults(k=3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a3421cf0-9067-43fe-8681-0d3189d15dd3",
+ "metadata": {},
+ "source": [
+ "## Create the Graph \n",
+ "\n",
+ "Here we'll explicitly define the majority of the control flow, only using an LLM to define a single branch point following grading."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "10028794-2fbc-43f9-aa4c-7fe3abd69c1e",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAHpAL0DASIAAhEBAxEB/8QAHQABAAIDAQEBAQAAAAAAAAAAAAYHBAUIAwkBAv/EAFcQAAEDBAADAgoEBwwEDQUAAAECAwQABQYRBxIhEzEIFBUWFyJBVZPRMlGU0iM2YXGBkbMzN0JSVnR2kpWhsuFTVHKxJCU1Q2J1d4KWorTD1AlERXPB/8QAGwEBAAMBAQEBAAAAAAAAAAAAAAECAwUEBgf/xAA7EQACAQICBgYHBgcBAAAAAAAAAQIDERNSBBIhMVGRFBVBcaGxBSIyYdHS8DNTYoHB4SNCY3KSosI0/9oADAMBAAIRAxEAPwD6p0pSgFKUoBSlKAUpSgNY5k1nZcU25doKFpJSpKpKAQR3gjdfz51WT3xA+1I+dVRjdmt8mBIdegxnXFTpm1rZSSf+Eud5Iraeb9r92w/gJ+VeCv6QoUKs6Ti3qtreux2OvHQNaKlrbyw/Oqye+IH2pHzp51WT3xA+1I+dV55v2v3bD+An5U837X7th/AT8qw610fJLmi3V34vAsPzqsnviB9qR86edVk98QPtSPnVeeb9r92w/gJ+VPN+1+7YfwE/KnWuj5Jc0OrvxeBYfnVZPfED7Uj5086rJ74gfakfOq8837X7th/AT8qeb9r92w/gJ+VOtdHyS5odXfi8Cw/Oqye+IH2pHzp51WT3xA+1I+dV55v2v3bD+An5U837X7th/AT8qda6PklzQ6u/F4FmwbtBuZWIcyPLKNc3YOpXy77t6PTurLqteHcKPBzu/IjMNR0G2wyUtICQT2snr0qyq6ycZRjOO5pPmjmVaeFNw4ClKUMRSlKAUpSgFKUoBSlKAUpSgKfxT/kt7+fTP/Uu1uK0+Kf8lvfz6Z/6l2tLN4zcP7ZNkQ5mdY1Elx3FNPMP3eOhxtaTpSVJK9ggggg9QRXyGnxctMrWX80vNn1tNpU43fYTKoFK4wQG+IMjEYdkvl3mQ1xm582BFSuLBU+Nt9qorCtcvrEpSoAdSRXsrjlw4QdK4gYsDoHRvUbuPd/Dqts3s15zfPrRkeBWNttwyIhTnVsvbJiTISVgvtSGEq28AOdCRyq0dEKTrVeanT2vXVvAic9nqskvDPjFeMvyvObbcMWuceJZbo9FjzG2mezS22y0sNrAeUtTqitShyp5eVSRsHYrc4hxogZTkarFKsF/xm6KiLnx499hpYMphCkpWpspWobSVJ2lWlDmHSogxi2d4/e+KNotFr7OPlD8i5WvJ25jSUQn1wUNIS40T2m0utJ0UpUNK37NVF+G3Ce+2TiPh97Rw+GNR4lrmW+7TXroxKlypDiG1B91SVkrSVNFIUVFe3OqUgbrZwptN7Fs4+7vM1KaaXv4e83mSeE+5P4KXrOcSxO+Kjs24y4c+5xmURivmCSFJ7cLVyEkkpGjynlKquXE79IySyMzpVmn2J5ZIMO5BoPDXtPZOLTo949b8+qqO18JL/M8EFvh/JYbt+RrsCoJYddSpCH9EhJWglOidDYJ76mNn4u220WuO3nztr4eXlSQU2y73qJ2jjYAHapKXNFBVzge31TvVUnGLTVNbm++3YWhKSac32LmWHSoSeOPDgICzxAxYIJICvLUbRI1sfT/ACj9dbvGs3x3NESF4/f7XfURykPKtkxuQGyd6CuRR1vR1v6jXncJJXaN1KL2Jm/wX8fr7/1ZD/ayasSq7wX8fr7/ANWQ/wBrJqxK+5pfY0/7V5HzOl/byFKUrQ8gpSlAKUpQClKUApSlAKUpQFP4p/yW9/Ppn/qXa2KoMZSipUdoknZJQOtbj0TWxDjymbld46HXVvFtqZpCVLUVK0NdBtRr99FMH3xe/tv+VcrSfRuPXnVVRJSbe59rud2GnU4xSaew0viEX/Vmf6gr2QhLaQlCQlI7gkaAraeimD74vf23/Knopg++L39t/wAq83VD+9XJlun0uDNbStl6KYPvi9/bf8qqLglFm53nXFi1XW93RcTG7+LdADUjkUlnsgrSjr1js99Op/6q5MnrClwZZdeTsZl9QLjSHCOm1JBrb+imD74vf23/ACp6KYPvi9/bf8qdT/1VyZHWFLgzS+IRf9WZ/qCvRphpjfZtob338qQN1tvRTB98Xv7b/lT0UwffF7+2/wCVOqH96uTHT6XBmDgv4/X3/qyH+1k1YlR3GcHhYtNly48iZKkSm22luTHu0ISgqKQOnTqtX66kVd1RUIxgneyS5I49eaqVHNdopSlSYClKUApSlAKUpQClKUApSlAKUpQClKUArnfwYP31fCB/paP2Ca6IrnfwYP31fCB/paP2CaA6IpSlAKUpQClKUApSlAKUpQClKUApSlAKUpQClKUApSlAKUpQCud/Bg/fV8IH+lo/YJq5su4j4nw/8U86MosuN+N8/i/le4Mxe25OXn5O0UObl5k713cw+sVzT4N3GPAbZxN45PzM4xuIxcMlMuG6/d46EyWUxwVONkr0tA0dqGwNGgOuKUpQClKUApSlAKUpQClKUApSlAKUpQClKUApSlAKUqEXDiWl11TVit6ruEnRmOO9hF/OleiXPzpSU/l+q8YSnuLwhKo7RVyb0qtjmmWqOxEsrY/i9o8rX6dD/dX555Zd/q9l/W9WmEsy5no6JW4EC8ObgUON3A64iDGD2SWEKudtKR66+UfhWR7TzoB0PapKPqr5ceDNwXkceeMlixVKHPJynPGbm830LURBBcO/YT0QD/GWmvsJ55Zd/q9l/W9VU8IODTHBDKctv+MwLcibkj/bPokOLU3FTzKX2LASlPI3tW9Ek9E9egphLMuZPRK3A6gpVa+eWXf6vZf1vU88su/1ey/rephLMuY6JW4FlUquWs7yeOeZ+02yYgd6Y8pxpf6OZBB/SRUpxzMYORrcYbS9DntDmchSkcjgG9cye8LT1HrJJHUA6PSqulJK6s17mZTo1KavJG9pSlZGApSlAKUpQClKUApSlAKUpQClKUBX+e3RV4uZx1tREFDQeuPKddqFH1GD/wBFQCise0cqTsKUKwUpCUhKQAANAD2VhsrU7kmUuL/dDcik9OukstBP9wB/TUU443CVaODGdzoMl6FNjWOa8xJjuFtxpaWFlKkqGikggEEdRWlfY1Bblbm1t+uFj6LR4qnRTXeTesO83mBj1skXG5zGLfAjp5nZMhwIQgb11J/KQP01ztm3nHh+I4BbLTfbvcLrmc2PHuFxuN7cYJIjLd7NlwocEUuKAH4NGyBrvPMNFxSwrNLRwL4jM5VNeTZEohyLYx5wP3GQysOhLyXJCmmlLbIKFBC+bRBO+7XnsXdVpOy3HWVatOTW1eTuY8JBN4bhpnqj9mvowpZQFc2uX6SSNb307tVT2eYzKgcQ+FWH2/J8jhWeWm7LmqTd31yZSUttrSlbylFZ0pXQ72kEhJT3iLcTsnvXBzKuILtiu12nJg4NEmxo9znvTG2ZHjLkftwlxShsIbSpR16xCid7NQTKrq71u+Fzp6lc8Yrw34kv3BpDt7lQcfuVvkx58s5c/dH1KcaPYyI24zXYrSvlO21BOj3dBWoxzilk+SwFXAuyhP4dY3OcvkNt1aUTrygOMoacAI50gR3HdHf7s2e/VLDF4o6erGmwvGuydadVFmR19pHlN/TaX9Y+sEdCk9FAkHoa524Q2PiZc5GGZWq6Kft9wQ3Luj8rKHZrM1h1okhuGYqG2VBRSpIbWOXlKTzbJrpKrRk4O8d5eLVSO1E1xG/nJbBGmuNpYldWpLKFcwbeQeVxIPtHMDo+0aPtrc1A+F61CZlTQ/ck3BtQ0NaUqMzzD+4H9NTyvTVioz2dtnzVz5urHUm4rsFKUrEyFKUoBSlKAUpSgFKUoBSlKArXKYKrHmDsggiFeAlSVk+qmShISUfnUhKSP/1r/TrMgsMDKrDcbNdGPGrbcI7kWSxzqR2jS0lKk8ySCNgkbBBq0rtaYl8tz0GcyJEV4AKQSQeh2CCOqVAgEKBBBAIIIFV/PxbILGsiO0MghAgIUlaWpSR/0gohCz+UFP8As/XtKONZp7fM6+jaTFRw6hHMjwDH8uxhOO3i2NT7OhKEojulW2+T6CkrB5kqGuigQfy1gWjhJidkxi649GtIctF15vHmJb7slUnmSEErW4pSyeUAb300Nd1SAzrkg6XjV6Sr2gR0q/vSsj++vzyhcP5N3v7KPvVXo9Xh5HvxKL23RH7BwjxXGX7M9At7yXrOXzBcfnSH1M9slKHerjiuYFKEjStga6araSsIsc+/zLzJtzcm4TLcLTIW8VLQ7FC1r7JTZPIRzOL2dbO9E66VmeULh/Ju9/ZR96tNYeIcTJ7heINqtt0nS7PI8UnstResd7W+RXXv0d1HR6vAYlFdqMTCeDWH8Orgudj9pVBkqZMZKly33g00VBRbbS4tQbTtKTyoAHQfVUiteNWuyyLq/ChNR3bpI8bmqQP3d3s0t8yv+6hI/RvvJJ/ryhcP5N3v7KPvU8oXD+Td7+yj71Oj1eAVSktzRFcW4H4ThV9Td7JZBAmNlwtJRJeUywV75+yZUstt72d8iR31NZUpqFHcffcDTLY5lLV7BXg0u9SzyxcYualnuVI7JhA/OVL3+oGpLj+CvmYzPvzjEh5lfaR4UcEssqHctRPVxY7wdJAPcnYCqlUXHbUdl3q/13mM9IpUo+r4Gdw8s79qx/tpja2Z1wdVNfZWdqaKgAls/lShKEnXTaTUnpSonLXk5HBlJybkxSlKoVFKUoBSlKAUpSgFKUoBSlKAUpSgFKUoBXO/gwfvq+ED/S0fsE10RXO/gwfvq+ED/S0fsE0B0RSlKAUpSgFKUoBSlKAUpSgFKUoBSlKAUpSgFKUoBSlKAUpSgFc7+DB++r4QP9LR+wTXRFc7+DB++r4QP9LR+wTQHRFKUoBSlKAUpSgFKUoBSlKAUpSgFKUoBSleUmUzDaLkh5thsdOdxQSP1mpSvuB60rVnKrKDo3iBv+co+dfnnVZPfED7Uj51fDnlZNmbWlarzqsnviB9qR86edVk98QPtSPnTDnlYsza0rVedVk98QPtSPnTzqsnviB9qR86Yc8rFmUb4aPG/iD4P2C2rK8MtVlultTJMa7eVWHnVMc/L2K09m6jSeYLSonfVTetda4M4UeHbxKxjN8leseOY/dbtmt1TKchqjSD/wAKWkNNoZAeB5ebl9U7J6jY3sfUTiHAxHiZg97xa83O3u226xVxnR4y2SnY9Vaev0kq0oH2FINcCeA14Mj+I8eb5fs3DMOLiTrjFtdkqCGZ8lRUhL7JV0cbSgKUFD+Etsg7BphzysWZ9MaVqvOqye+IH2pHzp51WT3xA+1I+dMOeVizNrStV51WT3xA+1I+dPOqye+IH2pHzphzysWZtaVqvOqye+IH2pHzp51WT3xA+1I+dMOeVizNrSta1klofWEtXSE4o/wUSEE/762VVcXHeiBSlKqBSlKAUpWFe7o3Y7LcLk8NtQ47khYHtCElR/3VKTk0kCMZZlknxt20WdzsZLYHjM8oC0x9jYQgHopwjR6ghIIJB2AYf5rW118yJkcXOYR60q4Hxh0/mUvfKPyJ0B00BoV7Y/Fdi2ljxhQXMeHbyXB/DeX6ziv0qJ0PYND2Voc84lwMDk2mE5b7jervdVuJhWu0spckPBtIU4v11JSlKQU7KlD6QHUmtJ1XBuFN2Xu7frh2H0VKjCjG73kg8g2w/wD46J8BPyp5Atnu6J8BPyqsIvhL2CZaMbnM2O/uuZBNmW+DCbjNqkF6PzBaVo7T1NlBA5ta71cqfWraZXxvawyGzMueGZYiEISZ82UzAbdat7Z3zB5SXTtSAklQb5+UdaxxJ5mb68LXJ35Atnu6J8BPyp5Atnu6J8BPyqIZVxltGOXW22uFbrrlF1nxPKDcKwx0vuIi70H1lSkpSgnoNnZPQA1CeGXHp6TwvxSbd4t2yjKryZrrdutUJvxpTLUpbZcWjbaG0oHZpJUU7JA6ndMSeZkOcE7Fy+QLZ7uifAT8qeQLZ7uifAT8q1mDZ1buIFmcuFuRJjliQ5ElQ5rJakRX0H12nEexQ2D0JBBBBINfxn2f2vhzZG7lcxIfL8hESLDhNF2RLfWfUaaQPpKOj7QNAkkAUxJ5mXvG2t2G28gWz3dE+An5U8gWz3dE+An5VS2O+EEtnJOJE3JI12tFms4tTUKzTYSBNS++lwFtCWyouKcWEcvrKH5QN1Mbdx4x5yHkLt7jXLEZFhipnToV8YS28mOrmCHUBtS0uJJSpPqknmGiASBTEnmZRVIMnHkC2e7onwE/KnkC2e7onwE/Kq8d4+w4OM3O/XPEMrs1ugtNPBdwgttmQlx1LaeQdqdHawSlfKoDfSt5mPFm0YTd51unRprr8OwS8jcVHQhSTHjqSHEDawe0PONDWu/ahTEnmZOvG1yT+QLZ7uifAT8qeQLZ7uifAT8qrJvwjrc/c4FvZw/LXZl0iqm2toQGk+UGU8pUtsqdARoKSSHezPUdNkA7NPHnH5OLWS8W+FdrrJvLjzEOyw4oM9TjKil9KkKUEo7MpIUpSgkdOp2NsSfFjXg+0nXkC2e7onwE/KnkC2e7onwE/Kq8f8IfHo1ojS3LZfEzXbwmwuWfxIeOx5imVPIQtvm1pSUjSklSfXSd65iNHxA8Id218LcvvdjsNxjZFYJDUSVarqw2HIinORSXHAl3lUgoWCChatkjp30xJ8WQ6kErlurx61OJ5VWyGob3ox0H/wDlftuhSMYUHMffMJKe+3rUVQ3B9XJ/zf8AtI1rpsKA5ag1642x7A3YGJWJ5Ib7ehIXHsLMdhyYhtkjtHF8rxbCdKSRpZJ5h03sCwYEvx+DHkhp2OHm0udk+jkcRsb5VJ9ihvRHsNWjWqR7dngGoVE4tXLBx3IGMkt3jLKHGFoWWno7oAWy4O9KtdO4ggjoQQR0IraVWONyzac9ipSQGbtHWw6n63Whztq/qdsD9fq/UKs6tJxStJbmr/p5nz1enhTcRSlKyMBWoy+0rv8Aid7tjfRybBfjJ39a21JH++tvSrRk4SUlvQ3FS2aem6WmFMSCA+yhzlUNEbAOiPYR3aqmfCQTdLdkGCXzHlpYv0J6W026JsNpamXG0hxsNSnG0uglKDsK2jkB0d9L1yWyqw+XJmto3YpDin3FIBJhuKJK1KH+iUdq3/BJO/VO06S+4vj+bQGWrzaLbf4X7o2idGbkt9f4SQoEfpFKsNV60fZe74d6PpIzVen6rKC4TWVrJ5HDO6Y3AnLgY7eb0m+Sbk/HU8JbrDgccKmllDgW670LWwAddADWTxl4UZJmmW5eHsUay+HdLUiHYJcu4ttRbI4W1pdUppR5ucrIWFoQonQTtOq6DtlrhWWAzCt0RiBCZHK1GjNJbbQPqSlIAA/NWTWFy2EtXVf1ssUDj2MZxw+yaz5HCxI31FxxmBaLlbk3COzIt8mMFaUFLVyLbV2hB5VE7TvRqEWfgDkFqtWD3m94Dbsxfgwp9uuWMTJEdSmQ7NckNPsLcPZKUArR2oHlX9ewOtaUuHRi+0rPEr/hvCrGokO7xsZ4Wypq3JSrGq4xmRvm5AvY5UrUUpRsgEA9NnW60vEWazxKONZBw7uVmzO6YjdkT3rXCubKg804060pHOFFKF6UVJK9D1DVxOxmXyC40hwjuKkg1+tR2mN9m2hvffypA3Qu4NrVvsOa7rw2zrMshy3KXMZTZbgm52G8Wq2zbgy4JaoXado0tbZUGyQrQJ2NlPXvIycz4TZjxokZfebjamsPkv2SNabTb5kpuSpxxmWJZcfUyVJSgrQhAAKjoqJA7q6OpS5TBi9jZSebxs94v8M8kx6bhKcWmuRG3I7kq7MPtyJLbqHA2ns9kIPIRzq5T1Hq1osyxXPOI2S5DdHcNcskeRgV1scZmRcYzrrk15TRQg8iylKVcp0reuh5uXoK6JpS5LpKW9lTQcGvjOe8LLiuFqFZcemQZ7vao/AvLRFCE65tq2Wl9Ugj1ep6jdXyeAd+RFx28T8OgZYq1Xm+Kk41PeYPjEWZKU4080tZLYcTyoVyqI2FEEpIrqmlA6UXv+t3wKIVwvku2vBXrLw+gYWuJlzF0uFtgvR/wcdDD7YeWpGkqV66BypKiN9N6NeeecI8kylPG5mLGaa842bcbS688jkkLYZTzJOiSj108u1Ad++6r7pS5OFFq31usUbxPg5LxExW1uyOFs5N8aL6oz0a/wAaPNtL/KkNutvIXopUSrmAV3IG0K3oWzhcW7wsPsUe/wAhEu+swWG7hIb+i7IDaQ6odB0KuY9w/MK3NYs25NQltNaW/LePKxEYTzOvH6kp/wB5OgB1JABNWjFzerFbSVFRbk2e1qjqnZ/YG0bPiaJE1Z10A7PsQCfrJe6f7J+qrSqN4ZjDljakzJvZqu00p7ctElDaE77NpJPeE8yjvQ2pSjoAgCSV6KjXqxXYreb/AFPn9IqKrUcluFKUrI8wpSlAKic7hfj0t9x9mM9bHnCVLVbZLkZKiepJQghJJPtI3+s1LKVeNSUPZdiyk47UyEnhRbySfK16G/YJp+Vfnont/ve9/bT8qm9K0x6nE0xqmZkI9E9v973v7aflT0T2/wB73v7aflU3rGuNxiWiC/NnSmYUNhBcdkSHA222kd6lKOgAPrNMepxGNUzMrLOrJiHDPGJmRZNlV1tFnicvaynpqiAVKCUgAJJJJIAABNYeIcHLyL9kM69ZnKuVilvNqscWAstmPH5ASXVnfOsk+zppII1zcqd5GsWQZzlmTw84sGPScGjSIjmPsqT40++42OdUhzm9VOlkBKeXYKVdSNFVjUx6nEY1TMyEeie3+9739tPyp6J7f73vf20/KpvSmPU4jGqZmQj0T2/3ve/tp+VVqxjLPDO6uR+IvEZxbF/vfiWMpbWqO5yrRzJZdPVJXsFIVoA+rs7WEjoGtXkGL2fLIjMW9WuJdYzEhuU01MZS6lDzauZDgCgdKB7jTHqcRjVMzI56J7f73vf20/Knont/ve9/bT8q88BvmWx13SJxBTZLfKXd3o1ket8gp8oRuUuN/g1kkOBIUCkEk9mo60Nmd0x6nEY1TMyEeie3+9739tPyp6J7f73vf20/KpvSmPU4jGqZmQpHCi2g+vcry6ne+VU9af706P8AfW9sOI2jGA4bbBQw64AHH1EuPOAd3M4olSv0k1uKVEq1SSs3sKSqTl7TFKUrEoKUpQClKUApXjLdLEV5xIBUhBUN93QVHvOeV/o2f6p+dASelRjznlf6Nn+qfnTznlf6Nn+qfnQEnqqMgtJ433vLsFzHBpLWCwFw1sXKTMLabq8CHVBCGyCWk6QCSrqdgjfd78TrL6U8JuWLzp820wbglLb79oe7GQpsKBUgLIVpKgOVXTqkke2sy7cRoGAWm1N3B9MWI7JjWqKp1Lz6lvOKDTKCralEqUQOZX5yfbQFgR47USO0ww0hlhpIQ202kJShIGgAB0AA9lelaS1Xt+dLDTiGwkgnaQd/763dAKUpQClKUBFs64Y43xIVZF5BbkzHrLPaudvfS4pp2O+g7CkrQQdHQ2nejobB0KxuGWX5Bl0G8HJMTkYnOgXJ+Ehp19LzUtlJ22+0saKkqSR3gdQfqqZVDc/4ZRM/umK3J263W0zccuSbjGctkkth08pStp1JBStCkkpOxvRUAQFK2BMqVCeE/Fe3cXbJcbjb7fc7Uq33KRa5MO7RSw8280rR2OoOwUnoTrejogiptQClKUApSlAKUpQClKUBq8pZlyMZuzUCQmJOXEdTHkLTzJacKCEqI9oB0dfkrhaTn+R8I+GmWWy4T8hj8So0S3+NOXy6+OwezelCOu4RHCFBtBK1bSU+oQgFB0d943m3t3a0ToTzYeaksLZW2ToLSpJBH6QaqDHfBsxPFoN1iQcYaXHukcQ5iZslcsusDemtvLWQ2OY6QND8lAVBbLbm/Ctu+5Bk1ymQsJj2SU5cEKyd69TA8kAtvRi7Gb7NWuca2UEqT6o1Uaxm75niOV3m1zpF8g226YVPu8aNeMhVdJTL7Smwh0LKE9grTp2hClJ2AQRquhca8G/FsShXOJbsdHi1yi+Iympk12UHI+iOy/DOK5UdT6qdD8lRS98IOGHCSVYH7na126XeZQxm3yHJkuS6+5KQUJilfOtQbKUnQWQhBGxynrQFaJXfcb4J8NZqMwvy7znL9lttxvUyet4w2n2i4tTCFkttrPRsL5eYlSVElXWt1xt4dJxfA7FbYuSZFL8ey+yJTKudxVMfiqMpCeZpboUQdnm0djYHTXSr0m8HLTc8GZw6ZYmZeNMxWobcB9znSlpsANjmKubaeVOlb5tgHe+taW1+Dbi9ngJhx7C6WROjXLmkXJ95wyI6uZhRWt0qIQR0STy/WCKA0HAoT8X435thy73db3Z4tugXOIbxLVKfjreL6HUB1XrFB7JJAJ6Heu+ui6hGOcP4lnzWdkogdjdZ0VqJJldsVc7TRWW08vMUjRcX1ABO+pOhU3oBSlKAUpSgFKUoCuOL0W4wJWKZKxn0TBrHY7kHrwm5dmmFPjuDsy04tSk8quZQCDzaCl70ohOrHqqfChumE2bgjf5nESzzb9iLbkUTIFvWUPOEyWg1ykONno4UKPrjoD39xtagFKUoBSlKAUpSgFKUoBSlKAVC+JV0za2LxTzLs8K7pkX6LHvZmLCfFbYrm7d9vbiNuJ0jQHOep9RXsmlfHnjD4VXhEYZnk7Fchz+YxccdunPuDEjRUurbJ5FK7JpPatLSQrs3OZCgU8yT0oD7DUrlrwBOInFLi3gF7yziJeRdbfKkojWbcJiOrTfP27n4JCeZKlKQgb7iyr8u+paAUpSgFKUoBSlKAUpSgIXxhumbWbh7cpnDuzwr9lzamRDgXBYQy4C6gO8xLjY6Nlah646gd/cZpVdeEDafLfCe8w/P70Y9ouOfOjt+w8U0+2dc/ata59dn9Mb59de42LQClKUApSlAKUqDZblEmXcHbNanjGSxoTpyOq0EgEMt/UspIKln6IIABUrmReMdbuNKdOVSWrEkN5zCx484G7ldokJ0jYadeSFkfWE95/VWp9LGKe90/Bc+7UVgWqJbEqEZhLaldVuHalrPftSzsqP5SSayqtr0Vss3+aXhZ+Z1FoCttkSD0sYp72T8Fz7tPSxinvZPwXPu1H6U16OV818pboEcxIPSxinvZPwXPu1xP4dHAmDxvznGMpwqZGXcZCkW29FwFoNsg+pKPME8/ICpKgnaiAgAHRrrilNejlfNfKOgRzH94JkmA8OsNs2MWa4pZtlqioisJLLnMQka5lHl6qUdkn2kk1vfSxinvZPwXPu1H6U16OV818o6BHMSD0sYp72T8Fz7tPSxinvZPwXPu1H6U16OV818o6BHMSRrinibqwk32KzvuMglofrWAKkzD7UplDrLiHmljaVtqCkqH1gjvqtSAoEEAg9CDWFEZkY0+qZYuVhzfM7AUopjSR7QUjohR9jiRsHWwoDlMp0p7Fdd7uvJW8TKegtK8HctulYFivcbIbWxPiFXZObBQsaU2tJKVoUB3KSoFJ/KDWfWTTi7M5bVtjFKUqCCqfChumE2bgjf5nESzzb9iLbkUTIFvWUPOEyWg1ykONno4UKPrjoD39xtaoXxhumbWbh7cpnDuzwr9lzamRDgXBYQy4C6gO8xLjY6Nlah646gd/cZpQClKUApSlAeM2UmDDfkr+gy2pxX5gN1UWLBarDDkPHmky0eNPr1oqcc9dR/Wo1bs2KmdDfjL+g82ptX5iNGqixYrRYYkd4csmIjxR9G9lLjfqKH60n9dav7F24ryZ1dAtrS4moy7PTiOV4ha34HaQMglPQTcO25RGfS0pxpBRynm7TkcG9jRA799IVa/CStOR48xPskBU+ZIyZONx4a3uz7QlfMJHNyk9mY+3x6vUDW/bUm42cNnOKvD6ZY4s3yXcu2ZlQbgCQqM+24lSVgjrvQUP8AvGtFbuAFssfFjHsptqxGtdos/k9FtBOu3bSGWH9dxIYW62T0P0Py15ToyxNbZuIjc/DHsFvuEt9DFqex2JNMN2ScgjIuKuVzs1uogn11NhWyNqCikcwTrW/G18XMtw258VLivHJmVYzZsjeVJlC6pD0GMmLHUtDDCweZKAVLKQpA9Y62d1JcF4Y5tw2kJsFqXjFwwxFxclMybgh7ygxHdeLrjHIlPItQK1hKysa2NpOtVrrrwm4hpPEG0WifjkexZlcHpL02SX1S4bTrDbC+RsJCFq5G9jakgE96qky/iWu95d9ruUa82yJcIbofhy2UPsujuWhSQpJ/SCKr25cXLj6WX8Js+Nt3JcJiLJmypNzRFWlp5ShzstKSS8lASSogp13dSRvIh8RcVwWFGxxtnIC3aGkQEdljlxfTytJCBpxEcpWNJ+kkkH2VE+I+DZDxnmWebaWrHAs7T8aXCvsyNKi3y3lt0KdDba20/TCSnSijoo7B6VBtKTa9V7TXZb4Xlmxu831piLa5lrsclcSa49kEaNPcW2dO+Lw1es6EnYG1JKikhIPQnc8UvCKPC6W1Kl2WBIxtbLUgTV31hiY+0rRUtiGoczvLvqOZJOjoGlj4ZZvgV+vUXHV4xPxi6XZ26hd4Q8JcIvL53mkpQkpcTvmKSVJI5uu9VouIfALKckm8Ro9rex1ULMG0f8aXNLqp0IJYQ2I6EpTylvaNhXMOXnUeVWus7DJurZ8ScniterjxRvOH2TFW7g3aW4L8m6P3IMNJakBRJCeyUSsBJISOigFbUnpvzwvi3feIM5mdY8NL2FvSnIzV9fuaGnXUIWUKfRG5CS3zJOiVhRHXlrOwLBbvYM+yvIrmuFyXqFa2UsxHVrLbsdp1Lu+ZCfVKnBynvIHUDurQ8NMCz7hezBxaDJx2dhMGSsx5cgvpuCIqnFL7EtgdmVp5uUL5gNAEp3UGi17q/v8A2Idwl4z5JZMTsrl/sUu5WGbkUu0HI37oHXkuOXB5pn8CQVFpJKGtlYI10TygE9IVSkPgnfI/CKz4sqVbzcIeTJvLjocX2RZF0VL5QeTfP2ZA1rXN03rrV10JpqSVpe4zeHkkxcmv1uB/AutMT0J1oBaudtz9fZoP5yasGq+4dxjKya/XID8C00xAQrewVp53HP2iB+cGrBr2Vt67l5I4Ok2xZWFKUrA8xXXhA2ny3wnvMPz+9GPaLjnzo7fsPFNPtnXP2rWufXZ/TG+fXXuNi1VPhQ3TCbNwRv8AM4iWebfsRbciiZAt6yh5wmS0GuUhxs9HChR9cdAe/uNrUApSlAKUpQCoNl2LyotwdvNpZ8YD2jOgo0FuEAJDzf1rCQAUn6QA0QU6XOaVeMtXuNKdSVOWtEqaBdolzCvF3gtaOi2lApcbPdpaDpST+QgGsupvecRsmRKC7naYc5wDQcfZSpYH1BWtitT6KMT9ys/11/eq2pRe27X5J+N15HUWnq22JHqVIfRPifuZr4i/vU9E+J+5mviL+9TUo5nyXzFunxykepUh9E+J+5mviL+9VH+FDjNtw97hCLKwq3C68QLVbJvYOrHjEVzte0aV1+irlG/zU1KOZ8l8w6fHKWfSpD6J8T9zNfEX96nonxP3M18Rf3qalHM+S+YdPjlI9SpD6J8T9zNfEX96nonxP3M18Rf3qalHM+S+YdPjlI6pQQkqUQEgbJPsrDhvSMmeVEsRS8rfK7cFJKo0ce083c4ofxEne9bKQd1MWeFuJMrCvIEN0juD6O1A/QrYqTsstxmkNNNpaaQNJQgAJSPqAFTalDart+9WXm7+BlPTm1aCsYdjssbHrWxAiBXZNbJUs7UtSiVLWo+1SlEqJ+sms+lKybcndnLbvtFKUqCCF8Ybpm1m4e3KZw7s8K/Zc2pkQ4FwWEMuAuoDvMS42OjZWoeuOoHf3GaVXXhA2ny3wnvMPz+9GPaLjnzo7fsPFNPtnXP2rWufXZ/TG+fXXuNi0ApSlAKUpQClKUApSlAKUpQCudvDG/d+Bn/afZf/AHq6Jrnbwxv3fgZ/2n2X/wB6gOiaUpQClKUApSlAKUpQClKUBVPhQ3TCbNwRv8ziJZ5t+xFtyKJkC3rKHnCZLQa5SHGz0cKFH1x0B7+42tUL4w3TNrNw9uUzh3Z4V+y5tTIhwLgsIZcBdQHeYlxsdGytQ9cdQO/uM0oBSlKAUpSgFKUoBSlKAUpSgFVX4RfB2bxkwu3R7NePIWTWG6MX6yzVthxpE1gL7MOpIO0HnO9DodHR0Um1KUBS3AXwg1cRJk7DsvtwxTihZU8tzsbp0l9I/wDuYxJPaNK2D0J5djqQQo3TVT8dvB/t3GKLAucKa7jOdWU9rZMmhDT8VzvCF/x2id7QfrOu870fA7j9Pvt/kcOOJEJrG+KNtRzKYB1GvDI3qVEUfpAgElHeNH6lBIF6UrQQM+x26ZndcSiXiJIyS1xmZc22tubdYad3yFQ+s62R3gLbJADiCrf0ApSlAKUpQClR7CeION8SLVIuWMXqHfIEeW9BefhuBaUPtK5VoP8AcQe5SVJUklKkk13CvY8Ja0We8Yjk+RYlY7NkKvGlNwxHN5bjn6KFLG+xUvWzrqErSpO+4D+LhekeEra8nxzHb5leDtY/fmoUu9wGjEXO7IgvssOH1wN7SpQ0QQn6SVFKror8AA7hqv2gFKUoBSlKAUpSgFKUoBSlKAUpSgFcS/8A1M+ItixPE8agCxzXc5XJE2y5GwhxhNpDa0lxSJAAC3FaA7FJ6bC1cumwvr/Jcri4020lbbsua/zdhDjjbjmtbJJ0EpGxtSiB1A6kgGB3q437KojsW4IszEB3XNCXD8dBHtClOEJV/UHy1VPZrSaS9/7bT0U6FSrtij5ZeB1xnl8PfCbsWQXa4PyW75JXb7tKkuFbjwkq6uuLUSTp3kcUTsnlNfaSuK8x8CHh7mcxyY9AYtUtfe5Z2PFEjrvo2hQbH6E1frMzKWGUNpyXaUJCQVwkKOh9ZJ2T+U1OpD7xf7fA36FVLXpVV+Usr/lIn+z26eUsr/lIn+z26akPvF/t8B0KqWpVPeFvxUPB7wf8svzLxZuTkYwIBSdKEh71EqT+VAKnP+4azfKWV/ykT/Z7dV/xl4Op492K32fML2/Mt8KWJrbMdoMAuBCketykbGln8o9hGztqQ+8X+3wHQqp8u+A/HfJuAecwb7YpsrxESGnLlaGpBaYuTKCdtuDRTvlUsJUUkoKiQK+5GPXyDk9htt5tjpfttxjNzIzpbU2VtOJC0KKVAKSSFA6UARvqAa5y4b8Acd4RuIexW22eBMb6omPWtEiQn8zzii4B+QKFW3Bz67Wtf/HUNmdD2eaXbG1BxsfWpklRUPr5CT9SevRhp7IyTf5/qkUlolWKvYsKleMSWxPisyYzyJEZ5AcbdaUFJWkjYII7wR7a9qyatsZ4xSlKgClKUApSlAKUpQClKUArDu90j2O0zbjKUUxYjK33VAbISlJUdfoFZlRLitz+YN05N6/Bc+v4nao5/wDy7rWlFTqRi+1otFXaREbamTI7S43AJ8qTdOSOUkhvp6rSd/wUA6Hds8yiNqO82lc6cfk2t3izZGcrtErKsadsUlMW0QnQVszQ6n8OtsrToFBCEunolQPdvdYTk6knJn0zapR2I6KSoLSCkhQPtBr9rjI4VeYtx4acP8qn49BgR8UMpuPfmHJECTcO3Pap00+ylx5DZbOyVfSWoDZ5qk0Xh3GGQcIcdu98iZlY5M69ra8TU4Ini/YBSYw264pbSFpICVLUNJCTvVUM1Wb/AJfrYdT0rjK/rlWmwScSjTo1nwxviPKtLyrgHVwo0YxUPMx3Ah1tQYLy9a50p+iD02DsMx4cjGeEeZx4uU2edapt3sbAtmMNuMR7Y8JzHOpAVIeLa1pW2SAUj1UnXWpsMZ7dm469rUX3LLZjcyzRbg+WZF3l+Iw0BCldo9yLc5dgaHqtrOzodPrIqjeLeF4nBu2K4BbsYxyI1OTNuvjN8U6mCz2YaS4otocQX3lcyOqlAgJKt1W1ot1jy7hfwSfynyffoMTLp1oXOmfhWTG3MS22VrKjyHs2NBRO+VHU99LCVVp2t9bPidpUrlPPLKxm3Gu6WG4XLFoVgttkgvWCHkMZ56KthQWHXo4bkspCkqCUlXrEAI0UgHd/cI7LJx7hvYbfKyBGUraj7bu7e+SS0pRU0UkrWSAgoSFFSiQne+tQaRm5NqxNMQuRsGTotmwLfdi4tpvr+DlBJWvl+oLQlaiB/CQo62tRqyKqObz+Wcc7PfaeVGda+rSub/y81W5Xrn60Yze9/ocXTIKNXZ2ilKVieEUpSgFKUoBSlKAUpSgFYl3tce92qZbpSSuLLZWw6kHRKFJKT/cay6VKbi7oFSW4yYxdttwI8qQuVt/QIDg16rqd/wAFYGx36PMknaVar/i9wde4nzbbIbm2RlMNtbZZveNx7shXMQeZJcKVII17FaPtBroHJcUiZK20pxbsSaxzeLzY5Aca3rY67CknQ2lQIOgdbAIhr+L5XAUUpYtt3bBGnm31Rlke3aFJUB/X/VWrgqj1oNJ8N3K+y3j5nbp6VTqR1amxkAxLgxjmP8OLRhtzhRcottv5lJ8rxGnklalqWSGykpQAVkJAHqpAHsqVMYxZoqrapm0wWlWwKTBKIyEmIFDlUGtD1NjoeXWxWb5Myz+TrX9oo+VPJmWfyda/tFHyqvR58V/lH4noVagtzRguYpZHoVxhuWeAuJcnVPTY6oqC3KcIAK3U60tRCUglWz6o+qsaHgWM2+ymzxcctMa0l1L3iDMFpDHaJUFJX2YTy8wUlJB1sFIPsrb+TMs/k61/aKPlUUw3Oblnd7ym1WrHyqXjc7ydPDsxCEh3lCvVOvWGj306PPiv8o/EnHo8UbzIMTseWsss3yzW+8ssL7Vpu4RUPpbX/GSFg6P5RXlJwjHJlrmWyRYLW/bpjxkSYbkJtTL7p1ta0FOlK6DqRvoK2nkzLP5Otf2ij5U8mZZ/J1r+0UfKnR58V/lH4jHo5kaa6cPcWvlvgwLljVnuEGAAmJGlQGnW44AAAbSpJCOgA6a7q3jDDUVhtlltDLLaQhDbaQlKUjoAAO4Cv5TactWdDH46D9a7ikD+5JP91bKDgF1ua93qazDibPNDti1Fbg+pTxCSB/sJSfqV9bAa9ppLvT8rlZaTRirpnhh9sN/yZF0I3b7UXG2V9fwkogoWU/WEJK0k/wAZah0KCKsivGJEYgRWY0ZluPGZQG2mWkhKEJA0EpA6AAdNCvak5KVkty3HDq1HVm5MUpSszEUpSgFKUoBSlKAUpSgFKUoBSlKAUpSgFc7+DB++r4QP9LR+wTXRFc7+DB++r4QP9LR+wTQHRFKUoBSlKAUpSgFKUoBSlKAUpSgFKUoBSlKAUpSgFKUoBSlKAVzv4MH76vhA/wBLR+wTXRFc7+DB++r4QP8AS0fsE0B0RSlKAUpSgFKUoBSlKAUpSgFKUoBSlKAUpSgFKUoBStTf8rtOLpYVdJiYgfUUtApUorIGzoAGtP6V8U97J+A792tY0qkleMW13EXSJdSoj6V8U97J+A792npXxT3sn4Dv3atgVcj5MXRS/h3ZjxP4acMIWY8OL+bQza5BReGBCjyC4y6Upbc/Ctr1yLAGk632uz9Gvnrwj8J3jeriBMhYhkQcyDMrklUlKrbFWJEpaezS4dtEI5dg+rpI5dkEbr6wZNmuC5fjtzsd1nIlWy5RnIkllTDultrSUqH0fqJri7wM/B4t3BfjNkuT5bOZcjWouRMckBJWZSVlSVSeVIJbPZ+ryq0fwiunQGmBVyPkxdH0RpUR9K+Ke9k/Ad+7T0r4p72T8B37tMCrkfJi6JdSoj6V8U97J+A792vxXFrE0JKlXdKUgbJLLgAH9WmBVyPkxdEvpXmw8iSy280oLbcSFpUO4gjYNelYEilKUApSlAKUpQClKUApSlAKUpQEA4j/AIyYr+eV+zTWNWTxH/GTFfzyv2aaxq5npD2qf9v/AFI+M9Mf+hdy82KUqNcR8+tvDDC7nkt2KvEoSUkoQUhTi1KCEIBUQAVKUkbJAG9kgA1y0m3ZHEjFyait7JLSqBtXhYw5Ld+ZlW60O3GBY5l8jNWTImLmy+mOjmWy442nbKztOtpUCOYgnl1Unx7jdMkX+yQ8jxkY3Av1tfudsmm4pkEoZQhxxD6EoAaUELCuiljoeuxV3Tkt6N5aNVjvXkWvSub79xiynM5fDK5wsemY5iV4yiKItz8qAPT4ymnilL0dIBS24AFgFSuiRsDpXSFRKDjvKVKUqVtbtFa3Jfxcuv8ANHf8BrZVrcl/Fy6/zR3/AAGr0PtY968ytP2495ZOOfi9a/5q1/gFbGtdjn4vWv8AmrX+AVsa+ln7b7z9Ne8UpSqEClKUApSlAKUpQClKUApSlAQDiP8AjJiv55X7NNRzKcvt2HRGZNxRPW06vs0iBbZE1W9E9UsNrIHTvIA/LUj4j/jJiv55X7NNY1c30h7VO+X/AKkfG+l7dJV+C82V/wCnLF/9Dkn/AIVun/xq0mcP2Xj/AIhc8Rtkm8Wu4uJbmRps6wTYzbLzLqHG1EvsoQr10p2neyN67ti26VzE0ndbzjqcYtSinde/9ipZOIcQMswTMLJkLGJQZFzssi3Q12dUggvuNLR2jiloBQj1h6qUqI69TX93jg/Nvlx4deMvxTb7DaZttuSEuKC3O3ioY/BerojaVbKtdNdD3Va9KnXfYTjSW7Z+6sc927hJxGiWvh/YrpOxuVj2F3SNLamxzIE2VFjtONoCmuQpDgQobAUeYjvHtsT05Yv/AKHJP/Cl1/8AjVYFKOet7RMqqqe2uWz9GV/6c8XP/M5J/wCFLr/8apZkKw5jNzWN6VDdI2CD9A+w91bStbkv4uXX+aO/4DWlG2LC3FeZEXFzjqrt+uwsnHPxetf81a/wCtjWuxz8XrX/ADVr/AK2NfRz9t95+lPeKUpVCBSlKAUpSgFKUoBSlKAUpSgNTf8AFbTlCWE3SE3MDCiprnJBQSNHRBFaj0U4p7ma+Iv71S2laxq1Iq0ZNLvBEvRTinuZr4i/vU9FOKe5mviL+9UtpVsernfNkWREvRTinuZr4i/vU9FOKe5mviL+9UtpTHq53zYsiJeinFPczXxF/ep6KcU9zNfEX96pbSmPVzvmxZES9FOKe5mviL+9X4rhNiS0lKrKypJGiCtZBH9apdSmPVzvmxZH8MMojMttNJCG20hKUjuAA0BX90pWBIpSlAKUpQH/2Q==",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from typing import List\n",
+ "from typing_extensions import TypedDict\n",
+ "from IPython.display import Image, display\n",
+ "from langchain.schema import Document\n",
+ "from langgraph.graph import START, END, StateGraph\n",
+ "\n",
+ "\n",
+ "class GraphState(TypedDict):\n",
+ " \"\"\"\n",
+ " Represents the state of our graph.\n",
+ "\n",
+ " Attributes:\n",
+ " question: question\n",
+ " generation: LLM generation\n",
+ " search: whether to add search\n",
+ " documents: list of documents\n",
+ " \"\"\"\n",
+ "\n",
+ " question: str\n",
+ " generation: str\n",
+ " search: str\n",
+ " documents: List[str]\n",
+ " steps: List[str]\n",
+ "\n",
+ "\n",
+ "def retrieve(state):\n",
+ " \"\"\"\n",
+ " Retrieve documents\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, documents, that contains retrieved documents\n",
+ " \"\"\"\n",
+ " question = state[\"question\"]\n",
+ " documents = retriever.invoke(question)\n",
+ " steps = state[\"steps\"]\n",
+ " steps.append(\"retrieve_documents\")\n",
+ " return {\"documents\": documents, \"question\": question, \"steps\": steps}\n",
+ "\n",
+ "\n",
+ "def generate(state):\n",
+ " \"\"\"\n",
+ " Generate answer\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, generation, that contains LLM generation\n",
+ " \"\"\"\n",
+ "\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ " generation = rag_chain.invoke({\"documents\": documents, \"question\": question})\n",
+ " steps = state[\"steps\"]\n",
+ " steps.append(\"generate_answer\")\n",
+ " return {\n",
+ " \"documents\": documents,\n",
+ " \"question\": question,\n",
+ " \"generation\": generation,\n",
+ " \"steps\": steps,\n",
+ " }\n",
+ "\n",
+ "\n",
+ "def grade_documents(state):\n",
+ " \"\"\"\n",
+ " Determines whether the retrieved documents are relevant to the question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates documents key with only filtered relevant documents\n",
+ " \"\"\"\n",
+ "\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ " steps = state[\"steps\"]\n",
+ " steps.append(\"grade_document_retrieval\")\n",
+ " filtered_docs = []\n",
+ " search = \"No\"\n",
+ " for d in documents:\n",
+ " score = retrieval_grader.invoke(\n",
+ " {\"question\": question, \"documents\": d.page_content}\n",
+ " )\n",
+ " grade = score[\"score\"]\n",
+ " if grade == \"yes\":\n",
+ " filtered_docs.append(d)\n",
+ " else:\n",
+ " search = \"Yes\"\n",
+ " continue\n",
+ " return {\n",
+ " \"documents\": filtered_docs,\n",
+ " \"question\": question,\n",
+ " \"search\": search,\n",
+ " \"steps\": steps,\n",
+ " }\n",
+ "\n",
+ "\n",
+ "def web_search(state):\n",
+ " \"\"\"\n",
+ " Web search based on the re-phrased question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates documents key with appended web results\n",
+ " \"\"\"\n",
+ "\n",
+ " question = state[\"question\"]\n",
+ " documents = state.get(\"documents\", [])\n",
+ " steps = state[\"steps\"]\n",
+ " steps.append(\"web_search\")\n",
+ " web_results = web_search_tool.invoke({\"query\": question})\n",
+ " documents.extend(\n",
+ " [\n",
+ " Document(page_content=d[\"content\"], metadata={\"url\": d[\"url\"]})\n",
+ " for d in web_results\n",
+ " ]\n",
+ " )\n",
+ " return {\"documents\": documents, \"question\": question, \"steps\": steps}\n",
+ "\n",
+ "\n",
+ "def decide_to_generate(state):\n",
+ " \"\"\"\n",
+ " Determines whether to generate an answer, or re-generate a question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Binary decision for next node to call\n",
+ " \"\"\"\n",
+ " search = state[\"search\"]\n",
+ " if search == \"Yes\":\n",
+ " return \"search\"\n",
+ " else:\n",
+ " return \"generate\"\n",
+ "\n",
+ "\n",
+ "# Graph\n",
+ "workflow = StateGraph(GraphState)\n",
+ "\n",
+ "# Define the nodes\n",
+ "workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
+ "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
+ "workflow.add_node(\"generate\", generate) # generate\n",
+ "workflow.add_node(\"web_search\", web_search) # web search\n",
+ "\n",
+ "# Build graph\n",
+ "workflow.add_edge(START, \"retrieve\")\n",
+ "workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"grade_documents\",\n",
+ " decide_to_generate,\n",
+ " {\n",
+ " \"search\": \"web_search\",\n",
+ " \"generate\": \"generate\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"web_search\", \"generate\")\n",
+ "workflow.add_edge(\"generate\", END)\n",
+ "\n",
+ "custom_graph = workflow.compile()\n",
+ "\n",
+ "display(Image(custom_graph.get_graph(xray=True).draw_mermaid_png()))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "447d1333-082d-479a-a6fa-0ac0df78bb9d",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'response': 'According to the documents, there are two types of agent memory:\\n\\n* Short-term memory (STM): This is a data structure that holds information temporarily and allows the agent to process it when needed.\\n* Long-term memory (LTM): This provides the agent with the capability to retain and recall information over extended periods.\\n\\nThese types of memories allow the agent to learn, reason, and make decisions.',\n",
+ " 'steps': ['retrieve_documents',\n",
+ " 'grade_document_retrieval',\n",
+ " 'web_search',\n",
+ " 'generate_answer']}"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import uuid\n",
+ "\n",
+ "\n",
+ "def predict_custom_agent_local_answer(example: dict):\n",
+ " config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n",
+ " state_dict = custom_graph.invoke(\n",
+ " {\"question\": example[\"input\"], \"steps\": []}, config\n",
+ " )\n",
+ " return {\"response\": state_dict[\"generation\"], \"steps\": state_dict[\"steps\"]}\n",
+ "\n",
+ "\n",
+ "example = {\"input\": \"What are the types of agent memory?\"}\n",
+ "response = predict_custom_agent_local_answer(example)\n",
+ "response"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "91325c88-ec77-4c79-8a77-cb2e2842bcd4",
+ "metadata": {},
+ "source": [
+ "Trace: \n",
+ "\n",
+ "https://smith.langchain.com/public/88e7579e-2571-4cf6-98d2-1f9ce3359967/r"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1b80d5da-f698-40d2-a2fb-4eac89e35350",
+ "metadata": {},
+ "source": [
+ "## Evaluation\n",
+ "\n",
+ "Now we've defined two different agent architectures that do roughly the same thing!\n",
+ "\n",
+ "We can evaluate them. See our [conceptual guide](https://docs.smith.langchain.com/concepts/evaluation#agents) for context on agent evaluation.\n",
+ "\n",
+ "### Response\n",
+ "\n",
+ "First, we can assess how well [our agent performs on a set of question-answer pairs](https://docs.smith.langchain.com/tutorials/Developers/agents#response-evaluation).\n",
+ "\n",
+ "We'll create a dataset and save it in LangSmith."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "b83706ac-724b-46b1-9f08-66e6c4fac742",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langsmith import Client\n",
+ "\n",
+ "client = Client()\n",
+ "\n",
+ "# Create a dataset\n",
+ "examples = [\n",
+ " (\n",
+ " \"How does the ReAct agent use self-reflection? \",\n",
+ " \"ReAct integrates reasoning and acting, performing actions - such tools like Wikipedia search API - and then observing / reasoning about the tool outputs.\",\n",
+ " ),\n",
+ " (\n",
+ " \"What are the types of biases that can arise with few-shot prompting?\",\n",
+ " \"The biases that can arise with few-shot prompting include (1) Majority label bias, (2) Recency bias, and (3) Common token bias.\",\n",
+ " ),\n",
+ " (\n",
+ " \"What are five types of adversarial attacks?\",\n",
+ " \"Five types of adversarial attacks are (1) Token manipulation, (2) Gradient based attack, (3) Jailbreak prompting, (4) Human red-teaming, (5) Model red-teaming.\",\n",
+ " ),\n",
+ " (\n",
+ " \"Who did the Chicago Bears draft first in the 2024 NFL draft”?\",\n",
+ " \"The Chicago Bears drafted Caleb Williams first in the 2024 NFL draft.\",\n",
+ " ),\n",
+ " (\"Who won the 2024 NBA finals?\", \"The Boston Celtics on the 2024 NBA finals\"),\n",
+ "]\n",
+ "\n",
+ "# Save it\n",
+ "dataset_name = \"Corrective RAG Agent Testing\"\n",
+ "if not client.has_dataset(dataset_name=dataset_name):\n",
+ " dataset = client.create_dataset(dataset_name=dataset_name)\n",
+ " inputs, outputs = zip(\n",
+ " *[({\"input\": text}, {\"output\": label}) for text, label in examples]\n",
+ " )\n",
+ " client.create_examples(inputs=inputs, outputs=outputs, dataset_id=dataset.id)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a23f6bc0-2d03-488c-8f4b-747c93876788",
+ "metadata": {},
+ "source": [
+ "Now, we'll use an `LLM as a grader` to compare both agent responses to our ground truth reference answer.\n",
+ "\n",
+ "[Here](https://smith.langchain.com/hub/rlm/rag-answer-vs-reference) is the default prompt that we can use.\n",
+ "\n",
+ "We'll use `gpt-4o` as our LLM grader.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "0a63776c-f9cd-46ce-b8cf-95c066dc5b06",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langchain import hub\n",
+ "from langchain_openai import ChatOpenAI\n",
+ "\n",
+ "# Grade prompt\n",
+ "grade_prompt_answer_accuracy = hub.pull(\"langchain-ai/rag-answer-vs-reference\")\n",
+ "\n",
+ "\n",
+ "def answer_evaluator(run, example) -> dict:\n",
+ " \"\"\"\n",
+ " A simple evaluator for RAG answer accuracy\n",
+ " \"\"\"\n",
+ "\n",
+ " # Get the question, the ground truth reference answer, RAG chain answer prediction\n",
+ " input_question = example.inputs[\"input\"]\n",
+ " reference = example.outputs[\"output\"]\n",
+ " prediction = run.outputs[\"response\"]\n",
+ "\n",
+ " # Define an LLM grader\n",
+ " llm = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
+ " answer_grader = grade_prompt_answer_accuracy | llm\n",
+ "\n",
+ " # Run evaluator\n",
+ " score = answer_grader.invoke(\n",
+ " {\n",
+ " \"question\": input_question,\n",
+ " \"correct_answer\": reference,\n",
+ " \"student_answer\": prediction,\n",
+ " }\n",
+ " )\n",
+ " score = score[\"Score\"]\n",
+ " return {\"key\": \"answer_v_reference_score\", \"score\": score}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "960f1a01-7f8c-429f-83d0-052cea47b32b",
+ "metadata": {},
+ "source": [
+ "### Trajectory\n",
+ "\n",
+ "Second, [we can assess the list of tool calls](https://docs.smith.langchain.com/tutorials/Developers/agents#trajectory) that each agent makes relative to expected trajectories.\n",
+ "\n",
+ "This evaluates the specific reasoning traces taken by our agents!"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "deb28175-27a1-4afc-9747-2983e87fc881",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langsmith.schemas import Example, Run\n",
+ "\n",
+ "# Reasoning traces that we expect the agents to take\n",
+ "expected_trajectory_1 = [\n",
+ " \"retrieve_documents\",\n",
+ " \"grade_document_retrieval\",\n",
+ " \"web_search\",\n",
+ " \"generate_answer\",\n",
+ "]\n",
+ "expected_trajectory_2 = [\n",
+ " \"retrieve_documents\",\n",
+ " \"grade_document_retrieval\",\n",
+ " \"generate_answer\",\n",
+ "]\n",
+ "\n",
+ "\n",
+ "def find_tool_calls_react(messages):\n",
+ " \"\"\"\n",
+ " Find all tool calls in the messages returned\n",
+ " \"\"\"\n",
+ " tool_calls = [\n",
+ " tc[\"name\"] for m in messages[\"messages\"] for tc in getattr(m, \"tool_calls\", [])\n",
+ " ]\n",
+ " return tool_calls\n",
+ "\n",
+ "\n",
+ "def check_trajectory_react(root_run: Run, example: Example) -> dict:\n",
+ " \"\"\"\n",
+ " Check if all expected tools are called in exact order and without any additional tool calls.\n",
+ " \"\"\"\n",
+ " messages = root_run.outputs[\"messages\"]\n",
+ " tool_calls = find_tool_calls_react(messages)\n",
+ " print(f\"Tool calls ReAct agent: {tool_calls}\")\n",
+ " if tool_calls == expected_trajectory_1 or tool_calls == expected_trajectory_2:\n",
+ " score = 1\n",
+ " else:\n",
+ " score = 0\n",
+ "\n",
+ " return {\"score\": int(score), \"key\": \"tool_calls_in_exact_order\"}\n",
+ "\n",
+ "\n",
+ "def check_trajectory_custom(root_run: Run, example: Example) -> dict:\n",
+ " \"\"\"\n",
+ " Check if all expected tools are called in exact order and without any additional tool calls.\n",
+ " \"\"\"\n",
+ " tool_calls = root_run.outputs[\"steps\"]\n",
+ " print(f\"Tool calls custom agent: {tool_calls}\")\n",
+ " if tool_calls == expected_trajectory_1 or tool_calls == expected_trajectory_2:\n",
+ " score = 1\n",
+ " else:\n",
+ " score = 0\n",
+ "\n",
+ " return {\"score\": int(score), \"key\": \"tool_calls_in_exact_order\"}"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "909b097d-cda1-45ff-8210-afeb2d18b8ae",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "View the evaluation results for experiment: 'custom-agent-llama3-8b-answer-and-tool-use-d6006159' at:\n",
+ "https://smith.langchain.com/o/1fa8b1f4-fcb9-4072-9aa9-983e35ad61b8/datasets/a8b9273b-ca33-4e2f-9f69-9bbc37f6f51b/compare?selectedSessions=83c60822-ef22-43e8-ac85-4488af279c6f\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "529952314cd34ac1bb115840536921c3",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "0it [00:00, ?it/s]"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Tool calls custom agent: ['retrieve_documents', 'grade_document_retrieval', 'web_search', 'generate_answer']\n",
+ "Tool calls custom agent: ['retrieve_documents', 'grade_document_retrieval', 'web_search', 'generate_answer']\n",
+ "Tool calls custom agent: ['retrieve_documents', 'grade_document_retrieval', 'web_search', 'generate_answer']\n",
+ "Tool calls custom agent: ['retrieve_documents', 'grade_document_retrieval', 'web_search', 'generate_answer']\n",
+ "Tool calls custom agent: ['retrieve_documents', 'grade_document_retrieval', 'web_search', 'generate_answer']\n",
+ "Tool calls custom agent: ['retrieve_documents', 'grade_document_retrieval', 'web_search', 'generate_answer']\n",
+ "Tool calls custom agent: ['retrieve_documents', 'grade_document_retrieval', 'web_search', 'generate_answer']\n",
+ "Tool calls custom agent: ['retrieve_documents', 'grade_document_retrieval', 'web_search', 'generate_answer']\n",
+ "Tool calls custom agent: ['retrieve_documents', 'grade_document_retrieval', 'web_search', 'generate_answer']\n",
+ "Tool calls custom agent: ['retrieve_documents', 'grade_document_retrieval', 'web_search', 'generate_answer']\n",
+ "Tool calls custom agent: ['retrieve_documents', 'grade_document_retrieval', 'web_search', 'generate_answer']\n",
+ "Tool calls custom agent: ['retrieve_documents', 'grade_document_retrieval', 'web_search', 'generate_answer']\n",
+ "Tool calls custom agent: ['retrieve_documents', 'grade_document_retrieval', 'web_search', 'generate_answer']\n",
+ "Tool calls custom agent: ['retrieve_documents', 'grade_document_retrieval', 'web_search', 'generate_answer']\n",
+ "Tool calls custom agent: ['retrieve_documents', 'grade_document_retrieval', 'web_search', 'generate_answer']\n"
+ ]
+ }
+ ],
+ "source": [
+ "from langsmith.evaluation import evaluate\n",
+ "\n",
+ "experiment_prefix = f\"custom-agent-{model_tested}\"\n",
+ "experiment_results = evaluate(\n",
+ " predict_custom_agent_local_answer,\n",
+ " data=dataset_name,\n",
+ " evaluators=[answer_evaluator, check_trajectory_custom],\n",
+ " experiment_prefix=experiment_prefix + \"-answer-and-tool-use\",\n",
+ " num_repetitions=3,\n",
+ " max_concurrency=1, # Use when running locally\n",
+ " metadata={\"version\": metadata},\n",
+ ")"
+ ]
+ },
+ {
+ "attachments": {
+ "80e86604-7734-4aeb-a200-d1413870c3cb.png": {
+ "image/png": "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eGlmOlBpeGVsWERpbWVuc2lvbj4xNTMzPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6VXNlckNvbW1lbnQ+U2NyZWVuc2hvdDwvZXhpZjpVc2VyQ29tbWVudD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CvHPuSEAAEAASURBVHgB7J0FnFVFG8Zf6QbplhBp6ZCUELFQDFQUBETsDsxPFEwMFFBUEBBRJKRLOgUEpKW7uzu+93l3z+Xs5e6NvbvLxjO/3+49MWfOnP+cM+fMM++8c80lDcJAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiSQ6AmkSPRXwAsgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIwAhT9eSOQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQQBIhQNE/iRQkL4MESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAEKPrzHiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiCBJEKAon8SKUheBgmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAlQ9Oc9QAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAJJhABF/yRSkLwMEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEqDoz3uABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABJIIAYr+SaQgeRkkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkkCqmCM6euyAnTp2VM2fPyfkLF2OaDI8jARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARKIJQLXXNIQalqHjp6S0yr2Z0qfVtKkSSkpUnDAQKgMGZ8ESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAEYptAyKL//kPHJWXKlJIpY9rYzgvTIwESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESCINASKI/LPwRKPiHQZyHkgAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkEAcEQjaLw98+JtLH1r4x1FRMFkSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESCI9A0KI/Ju2FD38GEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiCBhEkgaNH/jE7ci0l7GUiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABBImgaBF//MXLkqKFEFHT5hXy1yRAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQQBImQBU/CRcuL40ESIAESIAESIAESIAESIAESIAESIAESIAESIAESCB5EaDon7zKm1dLAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiSQhAlQ9E/ChctLIwESIAESIAESIAESIAESIAESIAESIAESIAESIAESSF4EKPonr/Lm1ZIACZAACZAACZAACZAACZAACZAACZAACZAACZAACSRhAhT9k3Dh8tJIgARIgARIgARIgARIgARIgARIgARIgARIgARIgASSFwGK/smrvHm1JEACJEACJEACJEACJEACJEACJEACJEACJEACJEACSZgARf8kXLi8NBIgARIgARIgARIgARIgARIgARIgARIgARIgARIggeRFgKJ/8ipvXi0JkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkEASJpAqCV8bL40ESIAESIAESIAESIAE4pTAuo1bpUSxwnaO8ZPnyrhJc6VE8UK6rZBtu61xrTg9PxMnARIgARIgARIgARIgARIgAW8C8S76nzlzVg4fPCwnT56WS5cueecnyvo111wjGTKkk2zZs0natGmi7OMKCZAACZAACZAACZAACVwtAhD7x0/+W9Zt2CYvPNnChP91G7dZdiD4Q/x3AoV/hwR/Y0pg/4HDsnnbLjl0+Ki2oUSyZc0kRQrlk9y5ssc0SR5HAtESQP2G4HRoRhuRO0iABEjAiwDrDy8gXCWBq0ggXkV/CP47t++W3LlzSP78uSVFCv/ehS5evChHj56wY/IXzEvh/yreKDy1yMrVGxXDJSlbqjhxkAAJkAAJkAAJJGMCaNB++8NgI3D7LbWiCGOw8ofIjz/H8h8dAE7HQEywQeQ9e/ZsjL6FDx0+JkeOHpOC+fNIqlQpY3J6HnMVCezdd1BmzP1Xdu7ZJzfoiJIc2bMKDKMOHjwiCxavlOfat7iKueOpkxoBd2emc22o49hx6dDgLwn4JrBy9QbbkZy1goRef5w6dUb27DsgOXNcK5kypvddkLG4ddee/XLh/AUpWCCPpcrvsViEG0tJbdm2U9KlTSt5VKOOj3A16ol4Ff1h4Q/BP1u2zEHxRKeAExfH5smXO6jjYjPShQsX5KW3v/aZZKUbS0q7R+7yuS8+Nq5as1F69f1THrq3idSqfmN8nPKKc4DPhk3bpeh1BSR16itvp9XrNkvP3kOlXJli8uRj90Y5vt/vY2TRktXyZecXJU2a1FH2JbQVCP5gjfBU2+YU/hNaATE/JEACJEACJBBPBBzBP0LcvymK4O+dBUf8//bHP6yToPtnr3lH8bsO6+5Bw/+SLdt2y+nTZ9W6O7OUK11M7rurYdAC/sy//5XJ0xdIp45PmGDs6/vx2PGTAnG5eNGCfvPDnfFLAN/Rw8dOl8b1q8sDdze64uS3yk1XbAtlg692TqZM6aVAvlxyZ5M6UqRw/lCSCyvuHr3/YPCVL09OS+fcufPy6nvdpErF0vLYQ3fYNl/36Rc9B8qRI8ek89tPhXV+HizirtucTkqn4xJ8QhX+T+jI/rc7f2fl+sGbHST7tVmIOQYE8Fz0+32srFBR+dzZ8xLqeyQGp+QhIRKAkNer73A7KrlqBbFRf0yZ+Y+MGDsjWvr33dVAbq5TJdr9gXZs2rpDvv/5T2n5gL49q5YPFD3s/QOHTjAj5g/fetLS8v4eC+cEo8bPlMkzFsh7rz0uuXJeG05SV/1YX+/2+MrUV98NkhuuLyTPPv5AnJ/yatUT/k3tY/my4dInS5aMIaeKY3Ds1Qx582SXZk3rRvmrWrHU1cySZMmcUSqWL2kNuKuVkR279kr3n4bIYf3Y9hdWrNooS1as9Rclwe5zBP+qlUpL1Upl7IXu9NAl2EwzYyRAAiRAAiRAAnFCYP3G7ZbuCx0e9Cv4u0+OuAgQ/4MNED0/7zFAtu3YKw20kftkm+ZSvkxxmT1vqfQbNDbYZK6I5+v7ERbjfX8bfUVcbrh6BHarheCw0dPkYTXuqabfn4HC7HlLZNL0+dpBtCtQ1Cv2X1c4n7R79C77q1G5rGzdvke/7wcH/L6/IqEwNgwfM00mTp3vSSFFimusnQMXRk7wdZ+WvL4wjXEcQGH+wl0ZrPrddRuEfnRwut2VBXuaZavWqSuqi5IyZYpE2w4M9lrjMt7aDVvl32VrpH6tyvLuq23j8lRMOwYEHCEvQisonWy1gtioPwoVyG2iPoR9GI0i3KB1PNbxV1C9hTBEEMiXN6e9I5OCG3Rf7/akVs5Xs5640jQ7DunCh38glz6+To9jAvn/93VcbG6Dv8xbGtSIzSTDTgvDtK/mSANcwI7d+4O+jmGjpkrpEkViNDQ96JPEckRH8EePPRpACE+1vdes/pNrL34sI2ZyJEACJEACJJBoCDhWrxDGQg2wnIVLIFjDBeMnG/FOnTwjTRvVVCGutp2uXOni+u21T1bpCMTz58+rtX/on/K+vh93hvA9F+p1M37MCExXlz4o+2Cs7U+dOi1TZi7UkbOpZMeu/dLm4QjL+GDPjBEkldSQCAG/GL07Yco82bhlh1S+MX6MnHAPFityeaRJypQpr2jn+LpP77q1brCXyXgBCDidk97Rbmt8k81dEmzd5RwPobpwobySPl1aWazLDetWdXbxNwQCB9SVFwI6uOLLBUUI2UvWUR0hL6pW0NyE/+SmFcRG/XFD8esEfwjLV60XGI5WVu8atWtUSNb3ma+LhzFAMAYBvo5NaNt8vdsTWh7Dyc/VridCbymEc7VJ9FgMu121ZpN0aH2PDa3B8JTv+gyR/OqOqFWL29T9zTb5/c+/pIF+6GzYtMMqsHx5cuiogXpyvU70hoBOjXGT5sjSlevkzJlzUqZkUWl+x83m9gaNuk+/6W8f3el0QuNJOozniVZ3m9XEgMHj5S5Np0LZEjJVh0PN/WeZHYc4O3bukxt1+/06DGqICu4QsHPnulYee/AO9WOWzc579NgJGfPXbG08bpLMOpwXluyN6lWzfU6+b65dRdas32qWQwXy55KHmjeRrFkyyW9DJ8ry/9Zb3B69h6iLn/zayLjT1r3/VdFREYuXrpbRE2fL/c0aeu+2dQxdHKP7MSIADPFRc89tlxn9PHC0nDh1yoZi/TVtniDvTRrUlPLaAP7tz4myXS3hypQsZswdn7Wbt+60YU/r1QVRAS0PWMqhwRxsKFuqmMedjyP6R2y7V62KInqfIQAghDrkNdg8MB4JkAAJkAAJkEDCIhCTdz6EfljMwhquRIfCAS8IrjEQTp0+EyVu+0fvFoi811yTwoR/fCOWvqGoZMmUUabOXigZM6QTfLvVqVkxynHOCvyXur8fP/66nxxQN5rn1e9s5y/6SN2aFdSirqrAF+2QUVNk+869kk+/yapWLCN1b/KdppM2f2OHAFwt7dcyeaj5LUEluHnrLms77N1/UG7TjoJwA/wdI5w4EXEPBtNOCXQPRvdNvn3nHnVdMkYnKD6mbZUNdg8++kBT7ewoIB991UfbODdoW6euRHef/vLHOJ2z4rg8/0TE3AYQSMdqm2qNukaCm9galctJvVqV7HoCtW0QKTne9xDzrV7Sdun1xQpe0SnpjG4KprPSQOs/1FFrtf14u44USJ8+nQwZOdnK+NpIN79O+7Zc6evREJYly9dKFp2cupl24jjtY/jf/mPEZFmv+UuXLo22+a63DtC1uo6RIXAdC7cWFy5clM+7/2LlXLt6hDiItjfui6faRLiX/effVTJnwTLZs/eApl9Y3VfVljyRk2CjjXn6zBlrA/8xfJLUqFJObm3o+zlCG/WbHwbJLTdXt3i43k3a3hw4ZLy1kZ28Oxzcv05bvbW2xYeNnmrtfLiU8Pd8/dD/T9mq7t0QBg6ZKFmzZpTXnn3U1mNyTeG0/XFSuAQbPWGWagAb7J1RpUIpez4xzwiCv/Qtgp9/0ZW340J4nY54gPuXTVrf5c+XU+67o4HHVzreX6M0X6hDoCmgvX/3bfXNDZ5zr6ED062loJMxunrJTzaj7IL/fkfcv6wVXN6GyEldK4iL+iMKZK8Vf3U8ogba75VcwNWDh46aXoZ3Smb1tAGtzC22w0p96qyFsk/dMea4NqvUqXGj1kWVA6brRECH6GQdpbf/0BG5Xl0s1rupkpRSQ9lAYebcxQJ3Qc8/8aBpc6jHjp88KTW1/pqkLh3xSNatWSno7zZ/9VAgvTOQjuev3oju3R7o+teu3yJTlDtGNxbSORNQLg636Opa5GOo6qLLVHPFCIl7bq9/xWn81QnRpXtFIl4brnY9Ea/ufbyuPVGtHlOB+b+1m6L84QMDAVYLR44dl1/1ZY+H5Q/1vbpn3yEVpCNGBpw5e04/MA7JyHEzdftBqyTwUvhZh1HjwwEBwvtf0+bb8NTaWlEsXPKf/DbsL9unSdrxeLmOVYEZgjssu5x0T0c2Bo+dOGXxBg+fLLlzZLeX2oJFK+WDrr017ll9GHLL5i279EafYuni4UQeVv63UTskqkjRIgXMh9qcBUttv5M+HnLNgmTXicPQ29pXP8wRMNQqd6T/sArlSliD03b4+FdMOwTQ+JypVktbdTJnX2HKjH+sgkJHBVwpHdMPte/7DrMPCsQ/ePiIbNy8w4YuFy6YTye0O68ffNN1qPxgyZk9m/m5RcfCDK0AEeBy6If+w+WQ/uKDE8OEcb07tPEaSvA1GY8j+IeSDuOSAAmQAAmQAAkkbgJwcRETK/+YXHVxFSRSpU4pM+b8K9/+9Ie69Vki+/YfUiONDGrEkd2MP5xvxPn6vYfGHxp76CwYPGKKrN2wxedpne875/uxmvpMT58unQlQcO1SQIfPI92eatBxQr8t71VhBYYYEO12q2DGEPcENmsjtkQI8yuUVmOhetohc0PxwiaIhZNDzCMxa96/lgTaDgjBtFP83YP+vskzZczgGU2ASYpxD2bJnEnPesnaNWhjIfi6T7Ed7QPkGeGstrl+GjDC2ms11V8z0h4ycorMW7jC9jv3fnRtm+R230Osc+YbASDUbxiN5IiUBi3yHzosQwnL1EoXbeXyZa83t2Rgu3TFGk8STt2FtuFGFc0LFswr27R9jLYfyhHhjxGTTMSFkRfKc+qsRTJ/0Qq5TtuBe/cdNqM7xNusfrp36ggXdBwgoI2LNmGuyM4rtOEH/DFe686M0lTF/F2798qP2kY8c+asxcc9tGv3AflF3aZhhIm/CT5R/6IDAoK7E5bqeY8cPaEdVZddUTn73L9OW906JI4cl2uzRcxx4O/5gngN91sIKANn5E1Mrinctj/yMFifJ4wqwrNa6oYiZlzn3C+B0sfx/kJ05Y1joKH8+MsI2aI6AjpljquG0h3vqMjO8cH6fpo+e5HVgRD+Zs5dYvPh4FjnXvPWUvzVSzgu2OBbKwjeyDDY8yS0eHFZf0R3rYHq+ED7o0s3uu1ws/jjL8PNvVZZfRbx/A/QjmYIzgjo8EPdgvlK7te5ltKnT6vvnKmqWW2PLsko29EJ2U/1qWv1+PvubCgntaNz4JAJVndGiehj5Xik9udokqjHNm3eqaP0/pbCWp+iA26wdpqisyyY4K8eCqR3BtLx/NUb0b3b/eUZ36I/DRipHclHrZME9W9vXd+lI2ERoqtrx/41x1xkQtdEx81InRcBLuicEKhOiC5d53h/v1eznkjlL2Pcd5nAJhXLv+sz7PIGXfrkf8/aRwFEeDykv6rVfa9+f5rV/D131PdYDzgH5cqZzXrm0euGSarsIdSPrXLaQzxp2gKpX7uS9kjXs+j44MAkJrCKTxs5yS2sFd5/vb358kek6BpzLZo3ts4DVDZffz9Ih8oW0JEB99gLr/MXve1lieMxOdgG9UvbvvXdNlIA2w6qdcyMOYvFsZLAtto1Ksq9d96MRfmkWz/Zor3r6E2vqo3EbTv2aKW2U3sRKwacQARCPnrVBqnlxWvPtbL03P9y6OiDlvfrpCrVIiZVwYR16LHftWefdljktagXL1ySl5582CrUXBofIwMa1atqPt7wkL738Q+yOdIaYoZ+RJ7UYfHvvHK/lRMmO3734146GmK5z8nQ3HnhMgmQAAmQAAmQAAnEhMC6Ddv8+u7H/mACGpGPP9LMrJLWrd8m+EOA8NNCJ3XNGznhKbZhlOgHHTvY91GNquXkoy/7aiN1rWeYPOJEF+C+8t/la7SBeEmaRFq3wigFjSgMqa+p6dWoUtZGdMJKiiHuCcDqPYcatIQS8mjHTPZd+6xjKJTjEBeTO7//yQ9y4eIls47GNpQ9XAtBRAmmneLvHgz0TQ4LPVia582d03MPwpDKHXzdp+79WMYIZIx0btPyDqlSobS2fS7ZCIHJM+bbfezEj65tg1E1yeW+N8FOBX7UJ+6JYR0B12GFdXQGwD1ZKAGufSAMOxMzF9QOJFi0YhSRO+TNnV1efqqlWaXCAhPt0C3btdNLrfExd1yhgnk8IzUwUh4W5RDl4TboP23L1q9d2UakYztG1MOqe5saeKEdWbZUUTsVRn4UUMvwdi3vsvMUVWO0rt0HWucQ5sdDQDvyhQ4tlEfgUVhV1GIcYhEs0yHyoYOjfJnrg3a3hlHqjsu2QM+Xtcn1UVi+coO5OXFGrMfkmvCch9P2P6mjN+bOX2ZtdbTZEaBTrFFREdcTrLZgB/r4F115I+o07fBBmXZ++0kz9Dt8pIp8qZN4r1ePCkW1nvp7wXKrsx50jY7C/DfwrAD3UgjeWgrKkFqBoQn5X1zXH9FlKFAdH2h/dOlGtx3zkuCd0urB26S6dkjjnfJ1r9+17tisBrDXicp60uy2uja6EiNS8mod9VXP30yPcruriy59uMrGq66Bjs7EKKHS2pG2Qg198ayhgyHUcFHf4a/qSCCM+Fyq3jN6Dxilz8j2gPVaoHookN7pT8fLmiWz33ojmHe7N4fpsxcLOmSebnu/dp5mFuh8nT77SY1floi7DnDXtUjj74XLtf7IJK8+84gliXdBz95DrVyxIdC3ih2k/7zTdbYn1F+K/kGWTNHr8pkrGXf09NrL7wQ0htBggtU84vryWViiaCH70MAxjrXEnr0HJbv28qMCgY9WdAQgHNThPQjorXJ8eeKmxORrgYLTSMADhpDj2ohGAzob8PG1NrLRuEMbBghLV6zTIbBbbBkvQ4xKcDfqIK47Ab2GsKTAR7G7IvL+MHfiu3/T6Qv3fm2k9tHKB8ORvMONOjkdLHH6/jZGDh89Jod1KBXCOe1gcEKGDGnt4wrrzqTQaBQjwB8p3PqcUhdACPhwSK0WcnCb5AR8EIIzAwmQAAmQAAmQAAnEhMC6jdvktmgOhO/rEtpwQ5zoQigjBSDwYHQhrMnwrbZo6X/mWxsjGd99tZ3nFHC/CPEJIa+Kv5kzZ5Dd+0KzykfD0wn4xitWtICJfRBykAc0ePGtxRD3BPBd7bjLCOVsMT0ui1rEl1TrWISFS1ZJvrzqzvPeCNdCO/W7OZh2ir97MPhvctdNaLm58p/7PvXeu13FGQTHJzQYwl3CHBUqIRA4wV/bJjnc945gh7rIcVXmiP3OOlhhFAA6KSH4h+ba54wJ8TA8Q/sYAUIL2skQ1931CFxJoY2KUFgFfgRnJHzFcjfY3BJwOwaRpbK6kkFbFKGc1klwZ4s2K8T/GlXLyrx/VpjABXdXadOmtroY9+4uFdZwToxWQnAsY+FH2hH902sbMxjBH8dXUh/jGC2yUkV0TCy6b/9hNZBrgF1BBYyQd0Kg58tXnmJ6TeG2/eGKCwGj/Z3gvu5A6cOo0V/wV97bd+0xN8XOvYPfzm8/ZcmhMwPhhusL2S/+QWuB6I+653rVYBC8tZTg6yU7nP8iCcR1/eEPdKA6PtB+f2n72uekV1IFfgS8U155uqUnKnQ6WNxjlMpBtTo/fjxChzrvet94IvtYKKMifxqtq3r+PNRc9MHFdzV1pZgm0ujXxyF+N0Erg+CPAM8YCLD4DxSCqYf86Z3+dLxA9YaTN3/vdieO84v6AO7KHXdxqA+wvl2fd3dw17UYFYTyqaKGy05Ah6E7BFsnuNN1H59Qlyn6B1kyWVRAd3rWfR2Cm/ScWhYgnNehjOhlS5ky8gsm8oCMapXghDSpU9siPjpOaE8ewjXoK4w8BENO6taqaMMHbaf+S5nC/4vSiRfs78nI4XDWqIg8L1z84A+W/L6C5dHXjiC34WVevmxxGyZcIF+uKEf1HzTOLI0ihnCWtY9FDBWKacCD7b42pIOPNPi8ZSABEiABEiABEiCBUAk4RhvRHWd++9VCNbpOgeiO87cd3zLFritgf00b3eQZVQohC9bdCN7uKNKoG0hH2PKXtr99z7S9z1wmLlyy2kZWTpw6T1588iGP6ObvWO4LjwAasDAAwtB/p4yDSRFzMzguQ4KJ78SBa5WH72tiqxAUYYQDP8YwrAm2neLvHoyvb3JYXiOkUYtLJ8AlKq7Je24MZ7932yY53PcYVeEW/B0WsOjHH+o5iP34DVXwR1qwtkVbEj798ecOsD6tr1atwQRYj+P+nz1/iVlgwq1Mi3sam49qWPHDVQPmrIPP+3vUf/t2HYG+eu1m2awuYEqWuM5c9cCCFR0+1rEQ2d5NmSqFtbPz6CgDJ6RMEbzXYzyfEJCXrVxvzyk6DGChG2xwi9/BPl/utHE9MbmmcNv+p9TSHsH7WXfyFih993U7x7h//ZU33NE5Yqb7GCw7z3Zq18T2qSI7GOAuxQneWkp81UvO+ZPKb1zXH/44BarjA+33l7avfbjv4CI6g85L4ivAPRlcyNVS99yw1ocLuf6/j/UV1ec2GMW+83Jbc5O1fNUGWbZivUD/6vhCa/M57/OgIDc6nanBRA+mHvKnd/rT8QLVG8HkzzsOytm7YyTC+DdCV3Xiu+scvAsQHC8qWMb3gTsEWye403Ufn1CXo15lQs1lIsgXLNfXrttqFgjwIThhyly5o0mdKDnfqhPNOmFDpJ8vfGwUyh9h1VBcxXZnqB965OD7Pnuklb5zXGz+YpglQk0dpeBYEWDyKkx+kibN5VEMsXlOpNXi7sbS5cufzS2QkzY+XDDiAFYazsRJmMQsnACuO3U0A3zROhP7omfa+QgIJ20ci4mCvH1z4cOT/v7DJcvjSYAESIAESCBpEsB3SLAWs8NGT1Pxdbk8+/j9nlGf6ADIkyuHuZJEY9EJ+N6ByJ8yZQpzYXBABdsbVPRyBwifwQZ8h8I6CxMCwxgDcwl83n2AWUw7lrbBpsV4oRMoqj68h+pEn4fV73e50sX8zpvlTn2duu2E66dwQq3q5eVvdYWJOb7u0klVg22n+LsH4/qb3Llex6AIk08Xj5wTARaMmTKlD2q0dHK5703Q1xFJ7gALf0zii0l7MVIpJmK/kx6s+1EXPaITMuPXCYP+nGQufoIR/a3TQOclwdwmcCmLiYHh73+aTlaOCcUL5s+r89xllNHq6hXiD0R4tMsWL1ttEwY/cE/Ec4B9mOw3kxp9oQ2KYKMDtHPAmZvOyV8ov5jAFufG5NkVdD66mIpAwT5f7rzF9JrCbfsX0lENCJhjz5kwE77NYeHfQOc4DCf9wOWdx7QCuG+CUIf33SSd/BQjPgrki8gX8gE3SwgYxYHg1Am24vUvruulpKoVxHX94VVMUVad8oyujg+030ks2O8hjOSBMS8md3X0MrwfM+jIygpqzPqPjoyDJ42H743oNIeluHfwdS5n227V3o4eP2GdmejQRP3Vd+AYcxnmnizYO83YXg+mHopO7wyk4wWqN2JyLXjmMa8Jzg23SvjF3D5lShaLNjm8L9CBs3nbTk8cGEq4Q1zXCe5zOcvxUU9cfgs7Z+WvTwKwtMGkGPiDpRP+MGEVAvbBJ1y5MsWkbcs7zc8dJuVF5eAOy1TUnqV+ppar37+/ps3TD5RUZrUFlz3F9SNruvowxD4c1+fXkTJA5whwW6q404qNZTQ6Mfx7+LgZsmnLDntwMIwTE8EF2zPo9Hpu3LLTOguCyResI9CIcAd8KGEY5iZNBw8sKkxn0i13vFCWK5YvYRM5YbIkNFwxAR4mp4Jfs5gEfAw7Q14h7vfqO9yEfyetiG1/Rtnm7OMvCZAACZAACZBA4icA9z1o8DquMEK5IhyD7xAI/8GEqhVL6ujRC/pNOMoEWIgrEDnmqNUr/FxjKLMT4A5j6Kgplja+exBKRYr+jlUTLGzRMPIV0utw8FNnzqjByS5zrYEG6Q/9RsiwMdPMHQeGrUOUyRHpUtFXGtwWewTGT5knZ8+ds/m7ihSKGKIfKHW4fsI9gUmewwlwV5Avb051lbLcxNFg2yn+7sFA3+SpdQQ0Jq3erw1wuDZ1Jlj1vg7v+9R7P6ytMcnqn+p6Bc/L1Jn/mKU5BNpgQnK47536BwK/d0DbEG2dFzo8GHTnpHcaKLvVa7fYJK8QrTDxrPN3o07qi7Ye3MkGCjDYGq7z2/UZONImcT2sx8Bq23HrirYqBJ7dew5ISXU3k0It9cuULGKudlBXlXWJPyh/+LKfNmuh+ufeaxNt/tBvuPnNDpSP6PbDLRDmsYC/78o6kjymIdjnyzv9mFxTuG1/dJ7k17kR5sxfar6xN6p+MGDwBNmqIywQwkk/UHlXVJdIKNdfdNJUGEaOmTjLRnqkTZtGO2+ya52VQ7WURTqZ8xpZqnMITtNJfQuq2zvsiy4EqpeiOy667VG1gg1JUiuI6/ojOrbO9kB1fKD9aSMNW/9bs9kzCbSTtq9fdG7hnQJ3XnB1iAm80XkJi3CEjDoCAHPwbFB9CfXaZC8vFTgftq/XjlTYXXh/j+3U+qv7j0Nk7oJl9q11WNNCQEdCfIZA9ZA/vTOQjheo3sB1Bnq3e7PAs4s5PgYNn2Ts8Yv6uHKF6OtivCMqlr9BvzEO2OhVfCP8PvwvyZjx8iiO2K4TvPON9atRT6TylZG42gbrJMzqDuChBBxjblpCOSiW4+KDAkMI3aFYkfxmedBfZ/CGf/+W9zW13Zh8F5PK/KLb33zxMc8hDepUVkF9sX6cHDS/gq0evN3zQD/1WHP5ZfA46ff7GJ0w67xOApJdnmzT3NNz5UkkFhfwcD+n1mMDh02Ubr0GWcoYCtnyvluDPgs+OBYu+U9+GzpBJ1TKpxMVR0yKESiBerUqWc/olq27LSp63ZrrZMHDdAKnDz7vLfALesvNNXQ28zGBkop2PyppMIZP/wXaQZNaO1lu1jJoUCe4IaXRJqw7YM3/VNt79WX+p1StBL9g19hw2KfaNr/C+t9fOtxHAiRAAiRAAiSQeAhA1IDLC7jAgGiG9WACGso4Bi41gj3mukL55Zl298noCbPkD21kosGIxgmsy7ytucuoq4sjx45bpwIaqBg1CaENAd9qaFCOGDfTBGEYWXiHm2tVlt1qGYnJLW+5ubpOTFdPWj90u/ypow0wcSO+3cuWLiq1a1b0PpTrcUAAbQYYAaExnT4atwLu0yIuDJIwUWlshDrqqmDIyKkmnuE+Cqad4u8eDPRNjnYAJvPFJK4ff91fnnn8Po8lsft6fN2n7v3o9Hii9d02+XWP3kMknVpjYmJrt99xd3zvZUxWmNTve6f+gYuOEh2i1l/omDQrfxX9YxowqS3EWdQ73gHi+PyFK22CyZuq3ei9+4r1x7QOGjhkgnT54mfbB8H53jtu9sSDix9Y3ZZWP9gIRQoXUPEoreTQ+fJQlk64rXFt6/D8SztN/xwz3UZ+wJ2VM2+eEy+UX7Sjry9e0OYLcPx9h3K8O66/58sdz70ck2sKt+0PPebZxx+QfoPGyODhk03XgX//+5tFjKoIN31/5Y1OlgfuPqkd3wv0PfWrlSEmV4WgiIB89VUdpZ+6VkE+MfLjsYfuMMve6OafD1QvuXmHuoxRJ0lRK4jr+iMQ50B1fKD9eOYxb8uS5WvNH3ygdwPE96fa3Gs++7/rM9S0OehYNauWt6zim26HzncJLQ0jyvAOXqQuEZ2AUUl4P3/zwx/yeafnr/geQ2dWk4Y1bGTfuWHnzSi4SYMaNjm1k0Z8/UZXD+H7L5De6U/HC1Rv4PoCvdu9GeC75Kh2psDlG3Q++Pa/766bBa7E/QWUN76V8b0EI+w6+k2bV0fP7jtwyA6LyzrBV77iq564Ri0aghrru33PYcmTM2LCVF8ZDmbbHrXextC6bFoooQT0eB3XYS95IoduhXJsQoiLyWW+//lPHbbTSG+sSjrZx8kok+C684ihaqfVQiI6n3XuuLG5DKsMPNAYHhOTgI87DN/EQx1OwPAp+OWL7etHb2x6HQaPRkVsBse6H2lS8I9NskyLBEiABEiABBImAQj4sNg3f9dBimMxnRDTIQCXBvCLigl23QGW+6+8200qqpj2+CN32YRyGHbuy8AG30IYoenvU81xneA+Byy4kWZM3Ve40+Jy7BOA5eFkndC0uYqhaLDGZfDVTgnlHkTe/H2To1V64UKE+w5/1+HrPvWOD5+/6ACLadskKd/3EPedTkhYHaJOQycARjGF49bHuwxiax1+tVGOsOoON2DSzeh80jtp4x6FazVPiJRL8ubJ4de4K6bHOefx9Xw5+/z9BnNN3seH2/bHcw8ZCa6GfAVf6WOUxWq1rvWESK6wzs7v0nkClbe/63VGs4Wqafirlzz5jcFCUtQKEkr9EaiO97cf9yfcREG/mqVeLs46oyAj70nUNRCE3QEuxrDd1/eVv3sSzwms0tPrd5QTcL+5v8cQB+8cfOM576xw6xPnXM7viv822KgpZ90sSXSlVvUKUfIW03ooGB0vUL3hvNtDuXbEDVU7xL0BXdDfOwXpxoV+6OHvWojreiJmCq8rg6EsZsueTXbqcCyELOZTyb/FPyz8jx49IXvVfU5+nWAqKQQ0tLwbbO7rQsUT6k3rPj6my/5u+GDSdHzmBxPXXxw8fHFx/XGRJq7DsfgXueT3I9DfNXMfCZAACZAACZBA4iEASzcIYxD+n+/4hc8JMZ2rcRrHWA9HTEPjNHOmwJ/t/sSsYL6FcB7v4O+71Tsu1+OOQI/e2tFUtJBkV8tDCASYg2utirX58+TSDp9mYbv1CSbnwbRT/N2DOIe/+xDtJF/3oHfegonjFli8jw9mPSnf9xD6ESD84w8hppP22sFx/M89f0m4pwp0fyJ9iM4LtTPNu4cU7T7v+dzc+YnpcU4awTxfTlz3bzDX5I6P5XDb/oFEdV/p71VrWl9cI9x/RPjlR94Clbe/6w2UL6TvK/irl3zFD3ZbUtQKEkr9EaiO97fffX8uURfcJ1XQdwfU/96iv79Rd/7uSYj43nnxvt8QByNl3CHc+sSdFpY36FwccFHkDjCHraJzprjzF9N6KBgdL9Dz6bzbQ7l2b5bu64tu2X290cWJSbrRpRVoe1zXE/Fq6Y+LRa/aYfXXiNnd8cHqL+Dmz6A+RtFZ4H4w/R2TEPehl2jn7r3mU8491DAh5pV5IgESIAESIAESIAESCEzAbR2L2BDNSkROjgkXGbCadbZjLgBnWLxtjKV/+JaGS8nMGTOoa8icsZQqk0mIBPbuO6gT0O2yyX0hjl+rLkzg7x/uDK5m4D14NemHf27UYwhxUT+FnzumQAIkkJAJsP5IyKXDvJFABIF4F/0JngRIgARIgARIgARIgASSCgFY8yO4hX6soxMgrsR+pM9AAiRAAiRAAiRAAiRAAiRAAtERoOgfHRluJwESIAESIAESIAESIAESIAESIAESIAESIAESIAESIIFERsC/U/1EdjHMLgmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAkkZwIU/ZNz6fPaSYAESIAESIAESIAESIAESIAESIAESIAESIAESIAEkhQBiv5Jqjh5MSRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAsmZAEX/5Fz6vHYSIAESIAESIAESIAESIAESIAESIAESIAESIAESIIEkRYCif5IqTl4MCZAACZAACZAACZAACZAACZAACZAACZAACZAACZBAciZA0T85lz6vnQRIgARIgARIgARIgARIgARIgARIgARIgARIgARIIEkRoOifpIqTF0MCJEACJEACJEACJEACJEACJEACJEACJEACJEACJJCcCVD0T86lz2snARIgARIgARIgARIgARIgARIgARIgARIgARIgARJIUgQo+iep4uTFkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJJGcCFP2Tc+nz2kmABEiABEiABEiABEiABEiABEiABEiABEiABEiABJIUAYr+Sao4eTEkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQALJmQBF/+Rc+rx2EiABEiABEiABEiABEiABEiABEiABEiABEiABEiCBJEWAon+SKk5eDAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQQHImQNE/OZc+r50ESIAESIAESIAESIAESIAESIAESIAESIAESIAESCBJEUgVytXs2X80lOiMSwIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkEI8EQhL9C+bJFo9Z46lIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARCIUD3PqHQYlwSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESSMAEKPon4MJh1kiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEggFAIU/UOhxbgkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkkIAJUPRPwIXDrJEACZAACZAACZAACZAACZAACZAACZAACZAACZAACZBAKAQo+odCi3FJgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIIAEToOifgAuHWSMBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiCBUAhQ9A+FFuOSAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQQAImQNE/ARcOs0YCJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACoRCg6B8KLcYlARIgARIgARIgARIgARIgARIgARIgARIgARIgARIggQRMgKJ/Ai4cZo0ESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAEQiFA0T8UWoxLAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAgmYAEX/BFw4zBoJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJhEKAon8otBiXBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABBIwAYr+CbhwmDUSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESCIUARf9QaDEuCZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACSRgAhT9E3DhMGskQAIkQAIkQAIkQAIkQAIkQAIkQAIk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kQAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkEAiIkDRPxEVFrNKAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAv4IUPT3R4f7SIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESCAREaDon4gKi1klARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgAX8EKPr7o8N9JEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJJCICFD0T0SFxaySAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQgD8CFP390eE+EiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEkhEBCj6J6LCYlZJgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIwB+BVP52eu/bvuew9yaukwAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJJBACIYn+BfNkSyDZZjZIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgAS8CdC9jzcRrpMACZAACZAACZAACZAACZAACZAACZAACZAACZAACZBAIiVA0T+RFhyzTQIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQALeBCj6exPhOgmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAkkUgIU/RNpwTHbJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJOBNgKK/NxGukwAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkEAiJUDRP5EWHLNNAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAt4EKPp7E+E6CZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACSRSAhT9E2nBMdskQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIk4E2Aor83Ea6TAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQQCIlQNE/kRYcs00CJEACJEACJEACJEACJEACJEACJEACJEACJEACJEAC3gQo+nsT4ToJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJJFICFP0TacEx2yRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiTgTYCivzcR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BEiABEiABEiABEiABEiABEiABEiABLwRoNDfGx0eIwESIAESIAESIAESIAESIAESIAESIAESIAESIAESIIE4RIBC/zjUWWwqCZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACXgjQKG/Nzo8RgIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAJxiACF/nGos9hUEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEvBGgEJ/b3R4jARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgATiEAEK/eNQZ7GpJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJOCNAIX+3ujwGAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAnEIQIU+sehzmJTSYAESIAESIAESIAESIAESIAESIAESIAESIAESIAESMAbAQr9vdHhMRIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARKIQwQo9I9DncWmkgAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkIA3Agm8HXQ9duLMBddd/E4CJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJBBLCAQk9M/8WOpY0mw2gwRIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIwJUAw/u4EuF3EiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEoijBCj0j6Mdx2aTAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQgCsBCv1difA7CZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACcRRAhT6x9GOY7NJgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIwJUAhf6uRPidBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABOIoAQr942jHsdkkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIk4EqAQn9XIvxOAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAnGUAIX+cbTj2GwSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAEScCVAob8rEX4nARIgARIgARIgARIgARIgARIgARIgARIgARIgARIggThKgEL/ONpxbDYJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJuBKg0N+VCL+TAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQQBwlQKF/HO04NpsESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAEXAlQ6O9KhN9JgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIII4SoNA/jnYcm00CJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACrgQo9Hclwu8kQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkEEcJUOgfRzuOzSYBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABVwIU+rsS4XcSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESiKMEEsTRdrPZJEACJEACJEACJEACJEACJEACJEACARC4GXpbLl25Ltdv3pL//vsvgDMf7qyPPPKIJEmUQIKSJ5FECeM/3DB49Q8tAc4fket6zh+R48azok6AQv+oM2QJJEACJEACJEACJEACJEACJEACJBCrCUBgF3L+sqRKkVTSpkomEEQx+UcACpIr10KVX/o0KSj49w8bcz1ABDh/RL4zOX9Enh3PjBoBhveJGj+eTQIkQAIkQAIkQAIkQAIkQAIkQAKxngAs/CHwT5EsEQX+AfYWFCTgBn7gyEQCDxsBzh+R73HOH5FnxzOjRuC+Cf3Pn79gt9y5be28deuWHDhwUI4cOSa3b/9r7Q73ef36Ddm//4CcPPk/uiaGIxN7v6CvT58+ow38559/xF3fR6b1znIjc35Uz7lw4YLgeu53Cgk5Kz///IvcvHnzfjclztf/77//ypdfzpdTp07f02s5dOiwLF26/J7WGUhlgc67P/20X1asWBWhiiNHjsrvv/8h4OwuXb16VXDuxYuX3B32uS+Q8z218V73vc+LYgYSIIH7SgDz83fffX9f28DKHw4CsAjEO9AvvxyUGzduPBwXzau8JwQQ0id50oT3pK4HtRLwA0cmEnjYCHD+iHqPc/6IOkOWEBiB+yb0f/XVdtpSCGb+7//eCtfqGTNmS5EiJaVDh87SqNGL8swzpeWbbxbbeaAEGDVqnMnzrPTt21/zVKpUQ/bt22/niesba9eul2vXYr8FARQu/nJ//fXOUqdOQ+nTp792z5Ahw+3t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kgAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkEC0E6DQP9qRskASIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESuD8EKPS/P9xZKwmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAlEOwEK/aMdKQskARIgARIgARIgARIgARIgARIgARIgARIgARIgARIggftDgEL/+8OdtZIACZAACZAACZAACZAACZAACZAACZAACZAACZAACZBAtBOg0D/akbJAEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABErg/BCj0vz/cWSsJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJRDsBCv2jHSkLJAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIIH7Q4BC//vDnbWSAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQQLQTSBBIiSfOXAgkO/OSAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAncQwIBCf0zP5b6HjaNVZEACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACQRCgOF9AqHFvCRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiQQiwlQ6B+LO4dNIwESIAESIAESIAESIAESIAESIAESIAESIAESIAESIIFACFDoHwgt5iUBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiCBWEyAQv9Y3DlsGgmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAkEQoBC/0BoMS8JkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJxGICFPrH4s5h00iABEiABEiABEiABEiABEiABEiABEiABEiABEiABEggEAIU+gdCi3lJgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIIBYToNA/FncOm0YCJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACgRCg0D8QWsxLAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAArGYAIX+sbhz2DQSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESCIQAhf6B0GJeEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEojFBCj0j8Wdw6aRAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQQCAEKPQPhBbzkgAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkEAsJkChfyzuHDaNBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABAIhQKF/ILSYlwRIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARiMQEK/WNx57BpJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJBAIAQr9A6HFvCRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiQQiwkkiMVtY9NIgARIgARIgARIgARIgARIgARIgASiicDN0Nty6cp1uX7zlvz333/RVOqDX8wjjzwiSRIlkKDkSSRRwvgP/gXzCknADQHOH26g+LGL84cfkJglRghQ6B8jWFkoCZAACZAACZAACZAACZAACZAACcQeAhDYhZy/LKlSJJW0qZIJBFFM/hGAguTKtVDllz5NCgr+/cPGXA8QAc4fke9Mzh+RZ8czo0aA4X2ixo9nkwAJkAAJkAAJkAAJkAAJkAAJkECsJwALfwj8UyRLRIF/gL0FBQm4gR84MpHAw0aA80fke5zzR+TZ8cyoEbhvQv/z5y/YLXduWztv3bolBw4clCNHjsnt2/9au+Xatety8uT/3P6dPXvOzscNEogqAYxBjDV8MsUsgZUr18iPP/4Us5W4lP7PP//Il1/Ol+vXb7gcuf9fL126LFeuXInQkKtXr8pPP+2XixcvRTjm3OFu7G7atEW2bdvhzBbQ9qFDh+W7777365xTp05HyGfN6adPn4lwjDtIgARIIC4QCGQejAvXwzbGXgKwCMQ70C+/HJQbN2Lfc0rsJceW+SKAkD7Jkyb0lY3HvRAAP3BkIoGHjQDnj6j3OOePqDNkCYERuG9C/1dfbacthRDr//7vrXCtnjFjthQpUlI6dOgsjRq9KM88U1q++Wax5lm1ao1UrFjd7d+bb3YPV05c+AKh8r59++NCU6O9jWvXrlclTrQX7KbAyNQ1aNBQHWeHD//upkTuik4CU6dOlx9+WOm1yOi+V86c+Uv69Rsoly55F6B7bVQMHLxw4YK0a9dRvvpqoV065sl27f5PihYtJc2bt5YSJUqbebOLx/vH3dj94ot5snDhIrvMQDe2bdsuo0aN8XoahBRLly6XOnUahss3atRY0/aS8tJLLaRcuSpSq1YDOXbseLg8/EICJEACsZ2AP/NgbL8Gti/2E4CCvnTpivLCC82kbduOUrjwMzJ8+Ic0Qon9XRcnWohnNYb0iVpXgR84MpHAw0aA80fUe5zzR9QZsoTACNyXmP7//HNRUqZMqS2FFUv27Nl0G/9WrVorH344RqZOnSTPPltC90+c+LH07NlHSpV61giTauqffYLZgFVs9ep1pFq1Ks7dcWJ72bIf5Lfffpenny4QJ9obnY2Ekmbp0m8kU6aM0Vms27ICrQuCy6VL/bNqdlshd0Y7gYfhXmne/FX1cIKVf40a1WyGvXr1k6NHjxrPhNk6V+zdu09efbW9TJgwSd5+u4udDxv3a+yuXr1OevXqaxQR1yRRorsWZJ9/PlemTZshY8eOlAoVygm8ALp06SFdu/aU+fPnhGs7v5AACZAACZDAw0wAin/8vnfp8oZR9r+qKLZu3W72tZO8efNI3bq1HmY8vPYYJnDk+CnZuvNn+evc31Igdw6pWKaYURB4rnT7ngPy4/7DJs79NXkicwapUamkJE2S2D7h/D+XZNO2H+XwkZOSJeOjUrtqaUmSOJF9nBskQAIPDoF///1PDh4+Irt+/FXKP1dEsmZ6zOvF+TM/7PnpV1PeIbn9720p/UxBKZAnh9cyeZAESCAigfti6X/kyFEj6M+urfnzzyPhhP6HDx+Whg0b2AJ/ZGrcuKFq0+FW7S6NHj1OHn/8cWnR4iV3h6NlH8JprFmzTq3yb9++bZd54sRJOXToN/s7NtDO48dP2PsgwMMD+4YNm8NZ5u7f/7Ps2rVXEJZox45dqrywToJiBPX9+uuhcJYECG+0a9cetfZBvbCORpgSJITNWL58pfz++x9WMV4///orROtYv36jtsE1c0jIWUFIELQb148wS/juTLCCh7APLynOhDaAAc5bv36T7N9/QP79NyxM099/n1flDoSDCOly8OCvzlPdbkM5hHp27txt2nFbLl++LLt379W8EICiTHDE9aM8y/oiMnWh3QMGDJb33uvvti0oG+7W8B5AP/mbwGrZshXa17DcthL64eeff9Hr2r59p4ZgQT87k7sxFBoaqmMBn1ZC252hVcAY48XigXze+gzjGX2JdroLu2XVA/4Y0xh/uJ+dCf2BMmCZjzF89Ogx52Hdhqs6xtLGjZv9slyLzL1iVerpXrKOe/vEmAU/KBathL7APmfYsT/++PMOi4jXivM8MbfKtD5nz55uxvUWSZ8+2Nql42Lv3h+N51N7KVSooFpnFSlSSJWcmDecydfYRV6MCShXcV87x6FVDq5v/fqNOiate9Y6hk/sgxXiihWrJCQkxD5UqVJ52b59g/GeeMfehw2Mk0aNGkjVqpUlYcKEkjVrFmnW7EUzJ/wcK0MrhWs8v5AACZCAGwKe5kErq6ffexzHHOvumdA6F/MqQt5hPmd6+AjgeTB//nzSuvUr9sWXLPmMZM6cOcL7hp2BGyQQDQQO/PqnTJg2X5InSyLFC+WVbbt/lnnfevbC/X7VVlm0bIMUKZhT6hph/t8XLsqw8bPNc33YezK+jxj/uZy/cFlKl3hazv59UUZN+kJuOd6jo6HZLIIESCAWEPj196Nm/vjKKPl+Mn/75NLliGFqnc30Z35YsmKTLFy6TnJkzyTZs2aUz+f/ILv2HXQWw20SIAE/CNxTS38I+po1a6UxqdE2CH0Qnxpp585dMnnyeHnttbb63foHoRRCQ2TI8JhRBBS3dtufEHx/8cWXMn36JxIvXszoMD7//EsZNOh9yZcvr4YCeeyxR7WtKVKkkNmzvzDCsQMya9andpuGDh0hOXI8KX369FSBZ8uWbSRbtickKCjIWLe+bc6dIMWKFZG5c+cbofePEj9+fPnkk+nSvfubJk9OU9YcGTlyjOaBUiRNmtSmnumSLFky8xJ43ITIaGnCY9RQoSq+Q/jYtu2r8vXX30iSJElU6D1ixAdSr15tu02uG4jNPWDAIBM66RkV0EKIPXHiGHnuuZKaFe357LPZKliEwLNXrz6SNGlSvY7SpUup8gLXhZdXKHB69uxtLI67SpMmz+v5o0ePvyOIPyvgdODALyZUTnljmTzGeDYcVstfZJw7d56xXC4oefLk1vNc/0GgDavgI0eOmPMraDkQsN42D4wvvPC8CRlSWEOhVKtWRYWQOXM+pWOpcuVKpq4PA6oLdaM+eAW0b9/GhJgq5NocZdW0aUsVfGJM7tnTVYYMGeiVNQoZPHioscBeJiVLPqvXgPjm6FOUsXz5Cvn005lG0JteywUfjDEcf/TR9B7HEK79jTe6yQcfvGdCppTRtr7zTj9JmzatjBs3Sr9DeD916nRZsmShX32WIEECVUrg5PHjR5mxV0TLcf6DUqt16/Y6FuCxs2nTZunY8TX73kVomiJFCsuff/4pKA8KoJ49u0mbNq20GCgJnn++qbEGT6RWa+PGfeQ2fr1mvvMvMvcKTvV2LznL97R9/fp1vd/QF888Ezb/4PpxD+7cudl4LAXp/LRw4bfat2PGTDCeK5nM/TxRi4Tyxtt94qle537MD+vXh3/pwviHQvDJJ8OUp8jva+wiz/HjJzRcQIYMGbSfg4PTmft8mu1ts2TJMunf/z0pXryYnDlzRsucM+czvU6rjjZtXtf7D3MTlHrjx39ojz/kcU047powZ6ZLl9bMV3etwVzz8DsJkAAJxEYCmGu9zYPefu+hBPf0TIhrfe+999V4Ab/veEYuUaK4CesyJDZiYJtiiACesfFnJTzjz537lRp0OD0AreP8JIHoIrBk5WYpVsh4k1QPe6fIbCzzPxgzUyqVLSHp06WKUE3IufOSKlUKKV44rx4L+fsf2fvTb8Yi919JIPFlw9YfJXGShNKiSQ09XqxQbuk+YIJs331AnitRMEJ53EECJBB3CeTO8YTgD2n3Pt8Gnb7mhxs3QwWKxVeb1ZZiT+fRcuPFe0QWLFprf9ed/EcCJOCTwD0V+idOnFgWLPjChO8ZpwLt8uXLaoiHTp1eDye8QqshWOvU6S0VckOAvnjxAhUSul7Rxx9PM2UVNQK3Z1wPRcv3Q4d+MwLb9+XjjycI2gthLWJqIzZ2ixbNfNaxePFSeeKJrDJz5lTN++233xlB8Y96/YMH9zdxuS8YQX8KGTp0kB6HIG/o0JFGWDnNCLSLqBAYQuahQ0eZl8F+dn0Ik9GgQV0VulWqVF0Fr0uXfmsUCPGM8L23fG5CangT+q9atUb69n1Hy0Chb731tjnnSxX6w4ob3hNjxoywQyYNGTJMLeunTp2kbRg2bKSkTp1a5s2brZbHsK5v3LiZlC1b2nhdZNA8586dM4qIuSr0h1UbXnSRD2Gb+vfvbVyUnzcC60G2wFFPcvm3Zct2tRZfu/YH2/q5Xr3GUqbMc9K5cwc799at28wio1+rkBx9Vr9+Y/n++x+kdu0afteFwkaMGG2EkenMC/0rRuj5l12+tfHuu4OMtRWEuh/pda9bt8EIvN80QtKikjHj41a2cJ+WV8LMmdMkZ84cqqQpX76KWoa3bPmy5j1lQp6ALcY6lCyIe75kyffGnbulGfuex1ClShWUD4T+EMDCowLKr9DQULWqRvuQB8mfPtu1a7cJITNLr1FPcvNv48ZNJpxWVduiG7HioeSBogQx6pAgDF++fLG2ZezYifLRRx/rtaBt7777nlqx4Xoh+If3xssvtxKMaU8pMveKv/eSpzr92Q/O06d/Zv6mGOFMMVWs9OjRW71uoNDxh7k/9TjzwKOlb98B6mExYsT79iFfYxcZIUTCfZIlS2b1EHrxxRZGaD9J5x94ZvTu3U9mzJhqK7w6d+6q98SgQe9qPfBK6d27p1E6Vte5EGGHBg4cYqxSl9p9bzfIzQZ4IVwblKXvv/+emxzcRQIkQAKxm4C3eRCGEPBC9PR77+33HKHZVq5cbX77v9bnKzw74HkHRhoIbcn08BFo2LCJwNAlXrz4RvD/mXl2ChOuPnwkeMX3gkDI2fNSuEBOu6p0aVIZT2GREBPqx53Qv0r5EjJuyjwZOn6WIO/+g78bhUFpSXwnxGPI2QsSnPausgDvAGlSp5C/TD3u0ndLd8vUT9ea95lr7g7H2L6v57/ptWy82yRNkkgSGW9VJhIggegh4Gt+OH/hknoNBadNbVeIeea8ibIAb6IECeLb+7Gx4dQOWXR4mVy95d3DINxJ9+BLsgTJpd5TNaTs42Fhy+9BlayCBCIQiBnT+AjVhO24fv2GCpoQqiJNmjS6vW/fT2oBDy8AZ8qYMYO8887bxsK8hwpJW7Vqp0JNZ57//e+UWk+3atXSuTtatxE+Izg4WAX+KBiWyxDIN2kSZtHuqzLE6ocgFoJBhKipX7+Oscpv5fE01Jct2xMq8EcmPCAhvBE8IZzJUnJAuPrEE0+oNRgE/kiwEEOYFiSE8gAnCPTwB6UF0ujRw1VIifAr8+cvNELiW0ZQeVqPJUiQ0AiME6gAU3eYf1euXFULcuv75s1btZ5ffvlVY5AjhlvatGlszw3kg7UwrPyRIOjHNqzYPSW8SFvtRFgeJFgC/2tiuEj28dUAAD56SURBVN28eVO/w6Ue1tewZncmKGRgFY+UK1dOKVAA3H92Zgm37a4uhDxB3PgRI4Z4FGBCedG4cSP7OOqFJwbCFCG5KxfXDeXHrVuhasG3YMFCYxmTSgXDVqMyZ86kAn98DzJKIPDCWg9I3sZQ1aqVVOiPfBtNqBx4ajzxRBYTOmCH3jewwq9UqYI5KuJPn5UqVdIW+MO6zDl2IMxAat36FeNh0FHD80D5hT5DOB/c31YqW7a0jl18r1KlkioyoET5z7w97Nu3X5UGEPgjQWECzwYrIVyVVS8+PSVf94qv467lerpXXPM5vyNcDbxUPv54qobLEflPLd8h8EfyxdzdeHGW79xGf2AewfolOO+rr+boWEcef8Yu8iE8EAT+SBiDGL+7d+/R71BGYh+UswcOHNQ/xA/esycsjBYy4Xqh8EHCXAiPI3gPnD17Vvd5+4dwWLVrNzTt/lq9ijCvMZEACZBAXCPgbR709Xvv7fd8y5Zt+uzyv/+d1vn32LET5vcl/Bwc11ixvVEj0K3bW2qgA+Om1q1f13ERtRJ5Ngl4JoCwO3j/s1KiO9vXr4e9g1n7rU94s8Z7JJ4JFXnNhPC5KP/9a94Xr959F7h1+5Y+N1r58ZnQvGN6Ku9+CPydbfO0jXeXax4YeDqH+0mABLwT8DU/hN6RWbnOSVBEXr8RcU6KjQJ/EIASAm1jIoH7SeDuL/s9aAUsmL75ZpHGMoUlOQTax44dN8L9ftK0aRMjHKxotwIvTggHg9SkSSMjzKyo1usdOrSz80BYDWEhYknHVILwHC9dzoQXPn8TLLBHjhxqLFvnqPU8wmoMGtRfhbruykB9lvDaOg7L8xMnTlhf9dOyqA63884XCOP+w5OXSRDqv//+cN3GOQhDBM+Dvn0HypYtW431WC0TYiaDeghoJvMPgnbEsx8wYLAsWvSdCmwhiEVoHiQ8/KCdKBsKDSsVLFhQrZGs785P1I1JG0J7T6lDhzc0pAjyIpQPPBsgEMbYaNjwRVVs/PbbbxoT/MUXG4crJmfOp8J9xzWeM5YpnpK7uj78cKxanmOBUaQbd35Q+vcfrIoYeCdAuO3aPwiRAsEnkrtyYYHfqlU7FabCQwH5XfvP9TsE4gjhguRtDEH5c+HCPxrnFUJVWMtDILx69RpJnjyZCmYh6PW3z5xjG+FdmjQJ80RA+7p2fdP0Qz3j3bHAeOuMVYEvFFQIP+OanNdjCffR95dNfD+E7EL/OBNCvVgJludQviBBOQgvH3fJ173i67hrme7uFde+dj0H3ydNGiczZswyFu+D1f2+YcP65t7po1x83Sfuxou7OqBQadv2dbX469evtyoPnYx9jd1hwwZrsVBgOhMUZLjHkTCG0TewxHcm5LESvHss5SL25coVdt9hjCM8lacELwQwateutQkD1UZDhXnKy/0kQAIkEJsJeJsHEydO4vX33tvv+fHjx1WJ7pyDMc97m1tjMye2LeoE8MyI9MILjdRbdvjwUea39JOoF8wSSMANgZRByY3B112h/dU7xjwpg5JFyA3B29TZiyV/nieleePqehyLAA8bN0tyPplJF9tEea5W/ddMme7Ki1ABd5AACTzQBHzND6mMESQSlItWwvwB+SHWHWEiARLwn0AC/7NGPSfckxEWAvG8EQ4Dwsk+fQbY8a9RQ48efVTYOWrUULtCxJKHUBDhYpzpu++WGsvRmrZFsfNYdG1D4AXlAgSWmGSQYNmM77CYhcDTORnhOASbVoLguVSpZ1UxgdAWCHWC0BwrViyxsoT7RH0/mMVRneno0aO2Fbhzvz/bsMTFnzMdOvSbsbZdoHHCLYtka+FV5MO1wQW9hQlfVKFCWY1R7rTExksohH0IH9S8+Ut20SEhiN+f3P4e6MaCBV9EOAWLE8O6vl+/d1RQDCFzypRBEfIhbrwzwTMCygJPyV1dr78e3psEAtAdO3aaWO4l5KmnntQ1FTJlyqiKKnhTIIHVsWMnzPEc+t1dud9+u1gXHoa1v5UQ39/f5G0MQaAOIULYwqwbzboQb5m1FP42gtVOarVdoUJ5e9wG2mcIV7Rx4+oIzYQAF/1hhY/CoraTJk2JkM/dDngx4H7GYr/WyyzyYS6wUt++vcw90sv66vHT173i67hrwe7uFct7Ad4lVnLe3/BQwv3w9ttd9A/jsGXLthr/H3x8MXc3Xqx6nJ9jx07QxYAXLJjrNhyWr7FrlQVvC2fCXGYtqg5eCCEwceJoOwvmtsuXL9nfz58/r143liIHSg3MgVmzZrHzuG5AGfXJJ59qSCx4FjCRAAmQQFwm4G0enDv3K6+/995+z3PnzqWer851ULAQPTzImB4eAl9+OV+V5AsXzgu37k2WLFmMp2SYV+nDQ4NXei8JPBqcRk6euuu5eep0iHnGFQlOlyZCMy6ad12E3yiY90n7WLYsj0tQimRy9PgZFfqjvB/3/2YMj0TLQYzucybuP/a7S21bV7gv4X3ctcW5D8/5CO/DRAIkEH0EfM0PQSmSGplBYp2TcmQL81I/efqspA9OHcF4Eq1CCJ3YaO1vhfeJPnIsiQQCJ3BPhf5o3ikTu/zxxzNqS//4408Tyz+bblv/EJ4EMbGxiGrdurU0ZtdsE/8ZwqUqVSpZ2VQAhgVBq1WrYu+LiQ2EWUF4mcmTp5qY5C00hEmbNq+bePIdVeiPl7Rp02boopglShTT+OQIYQPraqQJEyZpGJexY0eqFTlCwUDoaSVYY8OK3kqob/DgoUYpMssID5tpmBMs8goBfHQlyzIb/CD0RwiVVavW2MXDKjwkJESmTNlk1mD4xgiPU2oYkXr16theFTVr1tDrhjcGBOHr129UK/dlyxbZ4UPsAt1sJE8ephzACy3O95Ru3rypFvu9evXVmP5YOBbW/wixky9fHvu0b7/9zgih6+g+hA+BcLNMmVJ63N+6MN6c6fz5CybUz4dGsVTdXmgYYxDx6WFhD4t9WOShP4sVC1MCOM+3tuPHTyAQEqCfET4FSozDh3/XuPZWHm+fvsZQ5coVzZj5QDnCMh1/UFB9/vkXumaCVXZU+8wqBx4bR48e068QCmOMBJKwEB0UdlAo5cjxpFlIdo7xqrhrWeSprEDvFV/3EsLYIP355xF7vQjXupMYrxd4JWBMFSlSWF9aFi++q7C7ePGi8UqpYa5hqobkspRjyZKF3ePRxXzTpi2m/KIahgthwqwEq3soH/0ZuzgHcxPuFax1gbjTEC5g3Qgk3Ffw9MBc167dqxpGq1OnLsab6jF7zRFcF9Zk6dChvXq9wCujTJnSEdy3tcA7/9B2jEmEIXO2HYczZ86o3ijO/NwmARIggdhMwNs86Ov33tvvOZ5nW7dur6HzsJArlLLwtuvevYt62cVmJmxb9BHA8+WgQR/ou0D79q31WXPNmvW6zlObNq2iryKWRAIuBEoVLyjTv1givx85IRkzpJfla7dJvtzZJXXKMIvbdZv3mLAaN6R6xZKSyljxB5vFfddt2SPZsmZUy9ttu36Wi5euypNPhL3XlSiSTxYuWSdrN++SMs8UkmWrt5p34YRSKP9dD1JnE+rUKir4YyIBEoh7BK4aLyEr7A5kSX+fvyR/G8VgwoTxJcjIu7C9bNUWKV+6qGTKECy+5gco20oWKyCrN+6Sp/M/ZYzObsmWHT9JxTLF3MJBzHzGzXeLhjtJQO650B8hJHLkyK7ow6xMs4XrBixOi/jxkydP0YdeTBqw7v7ww+HhFuvduXO3hkuJ6UWtYO08ZcpHxrJ5oBFyT9e2woK8Zs1qug3vBcQOb9mytbHISarC8SZNnlcLcGTo1KmDCV/0rpQuXVEFYxA0Dh9+d/HNRo3qm7ApPUzc9uImLvhE9QpAuJABAwabhXTH6zVCoNe+fRutLzr+QdD68stNzcvla2qhC75Nm75gFgn7SovH9cArAYsXwzIbgvnt23caQV9nE3ZlkbEMzqYhOrDIHLw2EIYGwkIsKgoBpD8JXKtUqWzqbS6FCxcyQtNpbk9bvXqt5M6dU7p06awKIFjJIRxJx45vGEXF92pljBNbtWqhi5AeP35SvQ0GDOhrYuPm1zL9rcttA1x29urVXcMd1arVQPs4W7asZqyOU2G+S1b7KwSs33+/zIyBSsoOIXgqVqxgH/e14WsMoTwoRWDVb6VKlSpoGB4IDqyEsCpR6TOrnJ49u+tivAvM2gRYWwFM5s//2jrs87Nnz27SrVtPM3ZeMvdMEo0r/9JLTVW55u3kQO8VKJO83UuwTgcf3AdTp04KN7842zFs2BBjxf+OOV7OKHiSGO+kXkZwvlizIOzCkCEDzYLcb+m1YC0KeAzA+wIpuphDubBy5Sr904Lv/EP9mzZF9MZw5nFuYy0GLLzcv/8g3Q1PKSxajQTl3syZn6i31dSpn2p4q3Llygr6y0rVq1dTZUC5cpV1zQgoZ63QQVYe10/MH/DkqFattush49X0nc7vEQ5wBwmQAAnEUgLe5kFfv/fefs+LFStiFPWDdX6GkQCMHl55pYUaNMRSFGxWDBCAocHMmVP1OatatToanhEerm3atDLPGq/FQI0skgTCCBQukEsqlzstY8zivLdCb0v2Jx6Xdi3q23h+PvSHWV/vqgr9sbNj6+fls3nfS+/Bk/R9DNbwTRpUlry5suk5UAy0bV5PPl+wXOZ9s0pSpwqSDq0amHfGey5+0PbwHwmQQMwR+G7FRlm9fpddwefzl+t2vjzZ5I22TeSyke+t37LXKP2eUqG/P/NDvRpl5JwJrdx78GQ1vHumaD6pXLa4XQc3SIAE/CPwiBGq/+dP1hNnLkjmx1L7kzXa8pw9e06F3hA8x4YE4RXC11iW8s42IQwIFjpFGCJ3CcevX78miAXrLmGRTmesbORBfbh2K6yQu/Oisg9twuKyKVKE59ulSw+1THfGDYUlNhQXAwb0M+sA1LSrxfA5f/6CLuJr7wxgA+FxoMnFn7uEOps3b2YUDnfXcti7d59av0HYCaFniRJljMC/h0BhBKGrpz7wVZe7+j3tQ38h/E8gYxP9CQtBZ9x8T+W72+9rDLk7x92+qPYZysT1QxANz5XIJiggkFzHn6/yInOveLuX0I/w1vA0Bq32YD7Ci7cV2sbab33ieOrUqdxarkcHc6ue6PhEOC9wT5DA/YsX5jJYrMLTwV26ZhYIx/1kedG4y8N9JEACJPAgE/A2D/r6vff1e445OigoZYTnwgeZJ68tIgE8J2GsBAeni3iQe0ggkgR8vdPj+e76jVBJZkJr+JOwAPANk99bnO3LZrHfFHe8YP0pMy7k8cUxLlwD20gCgRKIiXHva35AaLB48cz6kB7eWwO9htiQPyY4xobrYhtiJwH3Ep9Y0tbY9pALS1hPCcIxTwIynOPruKvAH+d4qw/Ho5rC2hvxgQ6L5Hbu3NWE/WgvefPmUSH1tm07NBxN5cp3F1tG/RCUpk3rPjajP+3zpdBo1aqlen0cPPirxs2HoHndug26ICgE/s6EtngS+COfr7qcZfnaRn8FIvBHeVHtT19jyFebreNR7TOUg+uPisAfZQQq7Mc5SJG5V7yxT5Ys4gJlYTWF/+9rPvJ2PDqYh29N1L55Uj5apfoS5jtDlFnn8JMESIAEHiYC3uZBb785YOTr99zXHP0wcX6YrxXPSZF9VnqYufHao0YA70v+CvxRUwKztlOCZPG9VvqgCfy9XiwPkgAJBETA1/yQ2IQFYyIBEog8gVht6R/5y+KZUSUAy2SEYoKwHZbV2bM/Yce1j2rZgZ5/69YtwQKpv/32u1pk582bWxCyx0pYSBahWtKnD7Z28ZMESIAESIAESIAESIAESIAESMBB4ORf/0jG9Cl9erg6TuGmCwG8J/8vxKxL92gqlyP8SgIPNgHOH1HvX84fUWfIEgIjQKF/YLyYmwRIgARIgARIgARIgARIgARIgATiHIFzF64ILGdT/H97dwIv1fz/cfxDt9u+qYRKxc+WSGUp0U6LsiZ+UahEdvGzVBIRIklRVEiWZF8ipM1S+ZMkSZIoa7TvC//v+5szzb135t47d+Z2Z+p1Ho9778yZc77ne57nzLkzn+/3+znF01Ou7slS4XUbtphSjpQvWyJZqkQ9ENglAlw/4mfm+hG/ISXEJrB3bIuzNAIIIIAAAggggAACCCCAAAIIpJpAqRJFbfW6jabAk3qcMuVeQF5yk58cmRDY0wS4fuT9iHP9yLsda8YnQE//+PxYGwEEEEAAAQQQQAABBBBAAIGUENiydbutXb/JNm3ZRuA/hiOme3QVTU/zAf/0wtnfxyCGYlkUgZQS4PqRt8PF9SNvbqwVv0BS38g3/t2jBAQQQAABBBBAAAEEEEAAAQQQkIAC1qSm4VxAAIG8CHD9yIsa6yBQcAKk9yk4e7aMAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBCBQj6J5STwhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQKDgBgv4FZ8+WEUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBIqABB/4RyUhgCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgUnQNC/4OzZMgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCRUg6J9QTgpDAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQKDgBAj6F5w9W0YAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIKECBP0TyklhCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggUnABB/4KzZ8sIIIAAAggggAACCCCAAAIIIIAAAggggAACCCRUgKB/QjkpDAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBghMg6F9w9mwZAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIGEChD0TygnhSGAAAIIIIAAAggggAACCCCAAAIIIIAAAgggUHACBP0Lzp4tI4AAAggggAACCCCAAAIIIIAAAggggAACCCCQUAGC/gnlpDAEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBApOgKB/wdmzZQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEipA0D+hnBSGAAIIIIAAAggggAACCCCAAAIIIIAAAggggEDBCaTFsullv6+KZXGWRQABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgV0oEFPQv0qlsruwamwKAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEYhEgvU8sWiyLAAIIIIAAAggggAACCCCAAAIIIIAAAggggEASCxD0T+KDQ9UQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEIhFgKB/LFosiwACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAEgsQ9E/ig0PVEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBCIRYCgfyxaLIsAAggggAACCCCAAAIIIIAAAggggAACCCCAQBILEPRP4oND1RBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQiEWAoH8sWiyLAAIIIIAAAggggAACCCCAAAIIIIAAAggggEASCxD0T+KDQ9UQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEIhFgKB/LFosiwACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAEgsQ9E/ig0PVEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBCIRYCgfyxaLIsAAggggAACCCCAAAIIIIAAAggggAACCCCAQBILEPRP4oND1RBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQiEWAoH8sWiyLAAIIIIAAAggggAACCCCAAAIIIIAAAggggEASCxD0T+KDQ9UQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEIhFgKB/LFosiwACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAEgsQ9E/ig0PVEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBCIRYCgfyxaLIsAAggggAACCCCAAAIIIIAAAggggAACCCCAQBILpCVx3agaAggggAACCCCAAAIIIIAAAggkSGDL1u22dv0m27Rlm/3zzz8JKnX3L2avvfayoulpVqpEUUsvXGj332H2EIEIAlw/IqDkYhbXj1wgsUi+CBD0zxdWCkUAAQQQQAABBBBAAAEEEEAgeQQUsFu+cp2VKVnM9ilT3BSIYsqdgBpI1m/c6v0qlitJ4D93bCy1Gwlw/cj7weT6kXc71oxPgPQ+8fmxNgIIIIAAAggggAACCCCAAAJJL6Ae/gr4lyyeTsA/xqOlBhK5yU+OTAjsaQJcP/J+xLl+5N2ONeMTKLCg/8qVq0I1D38czNy2bZvNn7/Aliz5ybZv/zuYneXvjz/+ZD/9tDTLfGYgEAisWLHS9MOUvwJffTXP3n//g/zdSITSX331jaS8BqheS5b8GKHGGWdt2LDBZLdmzdqML/z7bO3adbZ+/Xr/TNfCn3/+JepPxALyOFPb0jV43bp1eSyB1RBAAIHdS2DhwkX21lvv7F47xd4kpYB6BOo70DffLLDNmzcnZR2pVGoKKKVPiWKFU7PySVJr+cmRCYE9TYDrR/xHnOtH/IaUEJtAgQX9L7nkUl9TBbyuvPK6DLV+6qlnrE6d+tajx9V29tnn2fHHN7TXXnszwzKTJk2x1q3PsPbt/2tt255lrVqdbt99932GZVL5ydSp023jxuTvQaAA5Ny583YJdV62pS9MzZu3tj597tglddyTN/L++5PtscdG50iQ6HN7wICBLmj+dY7b3dULqF5ffPFl1M3q2nfppVda3boN7MILu9hxxzV018LrM7zvV61a5Za5wl588VVfzooVf1nTpi2j/iQqL+vTTz9rDRo0tu7dr/D1Gjx4KDlfox5JXkAAgT1FYNasT23QoIf2lN1lPwtIYNas/7OGDZvaued2tG7drrBjjjneBg580NQhigmBeAX0WZGUPvEpyi9Rn7njqwlrI7BrBbh+xO/N9SN+Q0qITaBAcvqvXr3GSpcu7WuqoGyNGtX9Y/364IOp9uCDD9moUcPthBOO8/MfeeQxu/nm3i4IdYJVqrSv7+V600297H//u959ID7btm7dZnfeOcCuvfYGe/vt10JlpfKDa6+90e9L5coHJPVuTJz4nm9sOfroWvlez1i3tWXLFrvuuhusUKECa9vKd5NU3ECqnNv5bXvLLbfZjz/+aC+88Izp/TNnzly75JLuNmzYcH9tu/DCS3xPe/Xyb9XqVF+dihUr2sKFX2WpWteul1uRIkUT8iVu9uw57hr8sD3xxAjXIFHH9zK84IJL7OCDD7LTTz8ty7aZgQACCCCAAAKJEVBjvz4LXH/9Na7R/xJf6MyZn7p5l9oRRxxu7dq1ScyGKAWBCAJLlv5qMz/72v74a4XVOuxga3pSPffZMsKC/8769Iv59uW8RS7P/UarVmU/a9WsvhUrWiS0wsrVa+3jWV/aoiU/W9UD9rXTTmloRYukh17nAQII7D4Csb7f163faDM//9q+WfiDlSpZwpo0rGPVq+7vQb5ZuMSmz5wTEafFycfawTWqRHyNmQggkFWgQKKhSnlRo0YNX5sffliSIei/aNEiO+usM0MBfy3Uvv1ZvjVdw6o1PfTQMKtd+yj77387WFpamhUrVtT15L7Z9Ya5xPWS3eiXSdQv9bb//PMvfIohBeVmzJiVoehFi763yZOnmT6kZ57++GO5TZkyzaZP/8j+/POvzC/7dB56XT3lt2/f7l9XGho1fGg/vvzyK1uw4Nss64XP0Hpa7p133rN5877OkgpJvYI0NFgjI4Ky1GP+++8Xh4pRUFF1/Prrb+zvv3emUtq579t9GVomPE2Otvf553P8vv3f/31uSkOS3bR8+Z/28ccz7MMPP/H7rvQheq5JVipP02efzfam4elOYt2Wyrn33gdcI9F+duqpp+hplkmNT/L/9tuFue6tEe2Y5mSljetYKY3LpEmTMzgqRZV+gknLzZ79RfDU/1206Hvf2BXMjHbMVD8dbx33adM+zHH0i8qdOPF90/FTz/NgUjk6H1SXTz/9zNTrLPPIE7X0Byl9ZJnTlN25rbJUb40CiFRWTq/ntG2V/csvv2ZYTMbh71uZ6ou1zs/M+6oVly9f7t9HS5cuy1BOTk9+//0Pb6ztBe8vuc6Z86UbzdTdX8vU4l+nTm13rrbwx0JlPvPMk+48mGEVK1bIdhPvvjvJLTfH+vXrnWG5aOeIFtL2o103Ro58wpo1a+ID/lp2R5DhNBsz5hk9DU3Rzp3QAjxAAAEEdlMBXcv1f1Ep7fS/IfOU3fUxv/7XZK4Dz1NTQJ+9jjyypnXpclFoB+rXP96qVKniGv2/C83jAQKJFpj/7Q82bPRLVqJ4UTu29hE2a/bXNv71SVE3884HM+2NiR9anaMOsXYumL9i1Rq7b+gz7jvIv99p3fP7hz5rK1ets4bHHW1/rlhjg4Y/b9vcZ1AmBBDYvQT0/o/l/b5i1VobOGys/bTsN2t4/NG2f6V9bPSzb9r3P+z4nl2mTCk7/JBqGX5KlShu3y76yQqnk55s9zp72Jv8FtilPf2Vk7Jjx4t9oFA7Ns8FeRU01PTZZ5/biBFD7bLLuvnnwS8FIgcNGmL77VfJNQQc62creN26dUsfxJs8earr4VrEpaA41qUCOiNYLWF/ly5d6hoXOlvnzhfYK6+8bieeWN+POFBQsHPnrj7Xthowbr5ZIw96WocO5/htK+drv379XWqi431gUUHHRx55yK+vBZ599gXr33+A1ax5hAuWr/UjGLT/3323yEaPfsqXMW7ceNcD+Cg7/PDD/PPMv9QwcPHF3V0AfY0PzM2YMdN9UTjSRo58xPf4VXD/ssuudEHDilar1pGusWSoDwbrOPTt28v33J0wYaLdfvudduyx9ez33393oya22nPPPe1GYpSyYN9btGhuy5Yts79crw/l93788Ud8o8y4cS+5wOGXrid9IbfNJ+3GG6+1UqUOyVxN/1yvP/30Mz6oqcaBW27p7Rpriln16tXcEOYG9u6777uexWP8vmq7CvyuWrXarTPaB0Nj2ZY2+N57k1zjyRR7442X3JDowVnqNHbsc/bAAw9ZvXp1TA1P5cqV9UHW4sWLZ1k2mJHdMc3JSq46VqtXr7bDDjvUmd/lnnfyvbg++WSmvfzya/bSS8/5TanRo1OnLjZhwmt2yCEH+3lK8XLzzTeaRn5kd8zk+NJLr1qJEiVs8eLFrvwuoTKC/Qj+3nXXvW40yUSrX/8E16P8G39uKNCs91pwPHTuKLih81KjbPT6vvtW9Pllzz+/ky1atNifC3qPVqlSOSg64t9o57YC7+ef39lvR9v+4ouedvfdd4R6lef0esSNZZrZu/ftLiVOE7v66h6hVzp16mpDhjxgLVo0840uej/rfCxVqpT17Pk/dz0a5s8PraCRRAqu1617jL9u6XozcODdobKiPdA5OGLESG+qAJGuHyNHPurfM9OnZ/wipUC8GqAOOmhHg2i0MsPnazTLgAH3+XNJxyWYsjtHcrpuqEHwoosuDIryf9Ug8dJLr/jGMTVQZHfuZFiRJwgggMBuJrDV/T/X6Cpds/X5QR0Uhg590Bo1OsnvaXbXRzXw58f/mt2MeI/eHX0m1k8wqYPMuHEv2q+//hYa9Re8xl8EEikwYdInVq+2G03Scse1rIrrmX/PQ2Os2cnHWcXyZbJsavlfK61MmZJ27DFH+NeWr1htc776zra77w1pVsg+nPmlFSla2Dp1aOVfr1f7MLux3zD7dPZ8O/G4o7KUxwwEEEhdgVjf75+6RsXiLhbUpWNb27Bxsx1T61Br2bR+COCASuVNP8GkRoJ3Js2wq7u2twMrVwpm8xcBBHIhsEuD/grOv/zy8z51hIKtjRuf7IJrN9tVV12eJdClXv1XXXWdDzwrrcSbb75s6enp7kvW3/5LlgLaF198qQ9Wq1dy3753uuDXnW6UwOm52O3YF1Eg8LPPPra9994xOOK++x6wsmXL2vjxz/gAuxoi2rfvaCef3ND2338/H3Du0+dWO/PMdn5j1133Px/oV9Bv4cLvXNBsgMt/PswbqFe2cnrrxp+dOnV0AeFebvjuOXbPPf19kDdabZXHvKgbQjlmzHj/d/HiH/y9DVS+AsvPPz/e1W1ve/LJx30RCuQ1bnyKPfroEL9dGfbqdZs99dQoH1jXQldf3dPuv3+wa5DoG9ps48Yn2XnntfdB2Ysu6ubKe9oHeu+663aXg3yVC5KWdL3q+4eWz/xADQWDBz/sGh3u90F/vX733ff53smjRg0PLa76dOx4vq+fTHr1ut01DvRxwdY3nVfutqXCVM5tt91hw4cPdR9Gs35IVWBVowDGjh3tezMrqK2g8733DnLB3dtC9cn8QAHcaMc0WDaa1Zw5c13P+flupMg0PzJF58sTTzztg6jNmzd1273bj5ioUKG8qbe7juvkyVN8wF7HVef4ySef6Pctp2Om/Xv88UddAKJhUK0sf3VM1Dt8zJjRfht6XzVu3MI3lnTufIFfXl8wdXz0/lNDTaNGLVyDwztuiHln38ijHu/Tpr1v++xTzjdstWlzph1wQPR0VErXFenc7tu3v28wUDBcAWWNULjiimvde7uuK29/997O/vUsO5eHGW+++bZVq3ag8xjl13799bd8Pn5dp95++10/OmPChFf8e152p5/e3t/MsW3b1tluTTcZf+utV6xw4cLufb/IrXeOa4iakOU6pZFAffr08zf+vf/+AdmWGf6i7nWi+uiYBFNO7+vsrhs61jquagQLn8qWLeMbBDVao4gblp3TuRO+Lo8RQACB3UlA/4979brZ2rRp6RvLlartjjvudv8n3vYdQbK7PubX/5rdyZd92Slw1lkdXAeOH9x3j0Iu8P+069izI7i6cwkeIZA4geV/rnSBt52dt8qXK+O+p7iRrq7TV6Sgf4vGx9nDj4+3e4eONS07b8H3rsGgoRX5txfu8j9XWYV9dn4P03focmVL2h9uO5Gmt96ebaOemOoaUhM7aj/StsLnvfLSteFPszzWd5NiRdMt3X2WZ0IAgcgCsb7ff/vjL9dJsaiNHPuGff7lAhf7SHfpferama0bRdzAuNfes+oHHmA1qkWONUxdNtPeWvyebdy+M3NBxIJymHl3vT45LBHby1w/YvNi6fwR2KVB/02bNrsewpt8Wgv1rlWv57lzv/I9a9X7XI0CwXTAAfvZrbf+z376aZn7oDveB/gVoFQqH/Wumjlzlru55XM+4Kh1hg8f6QO9p53WyjcOBOUk6q9GEQQBf5X5ySczXeDuDJeSZGf6HQU/NXJBQf/Bgwf6AK3Sx6iH/Nat2+y3337z1VEqlQoVKvjAu2akpaX5AHSQ+sMvlOmXvmRu3brVz1UPeW3r+OOPdcH64T5NhwK0GhWhsn777Xcf9JenXP9xn9h0wdHjv//eHjLTTUYVFNdy8+cv8GUrlYd6f4dPapzRpP1v1qyJq+uOHumal3nSMV2/fkNomwrapqUVdkHPtAwpU7TMPvvsk2F11fGcc870dVWQtF2701yA9C2/X5F64EfalhoL1MDSteslvld2hg38+0T+6tGtfOWatF/t259lTz011j9XT+iVK1f5fdAM9XCXa3bH1K/ofkWzOvTQ/3j7Bx8c4nuw16xZM9RTXD20NUJDwW7tv4L+3bt382meLrusm5v/kW9kUe/9KVOm53jMVN/wgH8kp5IlS7qRK+N8Sh31YNcyOhd07gRTlSqVfcBfz9Wwo6B9cLPsuXPnuV7rDULnkhrAGjY80TfIaXk1ImjkiM49TQoaq/6RJqXUufPOvv6463UZKuis9DM6f3J6PbzMWLYbvp5y6mukiRq0mroRAWec0Tb0slJ61apVy40s+s3/6IXDDz/cNQrMcTcRb+3tMp/zwcotW57iA/56rnPgiCMO8wHzoHFS9dUImGHDhrtGh2r+mqblcjPJdvTop9w5c5YfmROsk9P7OrvrhkYZ6Dr09987jltQZvBc75XcnDvBevxFAAEEdjcBfT7RtV2TPhu0adPKf1b5888//cjK7P63xvO/ZndzZH9yFrjhhut8D399TuvS5XL3OXWkGyF8eM4rsgQCeRBQ2h19Xwum9H8fb9q0JZiV4e9GN+p9b9e5bMOGje6vu7GtyxC7fsPm0DLbtm8LfQYOZhZ23wmjlVcQAf+gXtn91eftjc6AoH92Sry2pwvE+n5f5UZJfr/kF2t7yol2/tktTOnFxox726pW3tfqHZ3x/9zPvy63r77+3m69bmfau8zeiQj4Zy4zEc+5fiRCkTLiFdj5nz3eknKx/qRJk+21197wQTz1/FYAST1hb731NtfTuoNLs9E0VIoCS02bNvbPO3Q42wUYm7qe8uNcDuxL/T0AFLBV4DuYTjmlme9NruD1McccHcxO2F+NMggmvXmXLfvZ54OfN29+MNuOOuoot0+FfKCzT587XK/umS4o2Mal9tgvw81kta6ChuGTvkRmN/XocY0PoiowrqCkeqRr20rfc9xxx/r0PWXK7Lg5clBO584dfc78Fi3a+O3NmTPHB8Nr1TrSL7J06TIfUNeNksOnQw/d2ctD87XNYJKD9j/aNHTocJe//D3/crly5fwIDfVav/PO261fv7v8F+O1a9f5QOmwYQ9lKKZ8+X0yHNNq1ar6banRJFLQP9K2dO8CNbyUcDnf1DCkSWll1BjQpctlPt2N/MNToWiZ8uXL+xRGeqyGmgEDBuqh3/exY5/wvcCzO6Z+4X+XDx6HWymgPn78sy5IO8bde6KH2y+za665wo3w+K9fXI1g2q4Csuq5rftTjBr1pO/9P336hz4FjRbMzTELP1e1TiQnpSPQSBk1+Jx00omuEap8huOs9cKPu56rXDW4aVJAv1GjHY1Bfob7pfejUh1o0usdOuwYMaByeva8Nkvvdi2nhiql78l8PFSfYF+ze11lhE+53W74OnqstAwPPHCvS1/0nL+O6D3bv//tvqFDqZvUez78faJ9UuojTZF8/Qvul/YjfDrkkEN8Oi/NUyNot26X+158t93Wyzc0ZDYPXzfzY6ULkrfSbYVPgVt4ffV68L7O7rqh65CO44oVK8KLdI1gK/3xV2NMbs6dDCvzBAEEENiNBNTIXajQzltiBQ21ujbqhurZ/W+N53/NbkTIruRSQJ/PNJ177tk+LdTAgYN84D+Xq7MYAjEJlC5VwnXQ2hm03+A+p2oqXSpr6lN9jxn1zJt25OEH2YXtW/rldBPg+x4ea4ccVNlqHX6wW69Ell79G12ZkcrzBfALAQRSViDW93uFfcraXy4lWJDS54S6R9qUj2fbwkVLswT935v6qR12yIGk9UnZs4OKF7TALg36t3W9YjUc+pxzzvcpZ9R7vXfvfj4HfQBx0029fWBx0KB7g1k+97sCwn/9teNmuOqNvnjxD6HX9UA3y9RUufL+/m9+/lJgTl/yTj/9tFDQVttbvvxP1wu2hA8yv/jiy+7muJN8Lm+9psClAoeaFHxT3nX1qFXDhya9pudVq1bxzzP/evnl5zPP8rnCmzRp7FLl9POvaX0F1oNJKWU0OuD22/u4AGV5n0IpfDSF6qFGikceGRys4nvjr1u3NvQ81gd9+tzi0pTckmE11UtD2jt16mhNmpzsjlFl33s+w0LuiVKcBD3O9Zpy26u+ymMfaYq0Ld3/QAHU8Gnz5i0+0Nq8eTM/CkP7rZz/4dOPP/4Y6tWuXnv6CZ8WLvzO9cKOfkzDl430WA0dSn2j1C1qNNHNn6+44hpr3rypr5P+PvbYKH9TQB1TNZSoJ71GXcjh7rvv8MXm5ZhFcnr99R1pYdQjMZgyj/AI5kf6W6lSJX8/h/DXgveg5qmH/kcfTQ5/OeJjNebo+KrxT/nyNel80Qif//znYN/Yk93rmQuNtl2d55s2bQotrt5JGvUSTGpYatDgBDeSpbF/zwwZ8ohPt/P++xP8qBml91LO5mDSPSfM3DceN0Xy9S+4X+Emmqf3uRp2NA0ZMszdE+F7l/JsXNRz3C8Y5deECRP9PT+Ulih8yukcGTFipHsfRr9uqPFy5sz/8/cxCcr99NPP/Lb0PN5zJyiTvwgggEAqCqgRVPdTCRrY1ZFA9zY68MCqPve6Gu6j/W+N539NKlpR59gFXnjhJRfYH+tSfu5I3RmUULVqVT86OnjOXwQSLbBvhXL2869/hor99bflrgOQ68BSfmcnu+DFNevWuxv0rrWjjjgomGXVq+5vpUoWtx+X/u6D/irvy3nf+Y5OKmfzlq0+yKf5kaZuXZoUSHqfSHUJn6fv/Urvw4QAAtEFYn2/77dveXd9WJSlwPBOFXpR+f5nz/3WLjo/+5S6bQ86NSHpfbJUKM4ZXD/iBGT1hAjs0qC/aqw0NPvvvyOIq8D9QQdVz7Ajynl/0029fI75du3auB7a213v2+d9z3r1htZ03XVXuV7DHdyH4mfcTXbP9cHigQMfdDfdrBvqfZuh0Hx40rp1K59aQ6MRFJScPv0jNwrhGtfL/Q3/5U+bXLLkRx/0/+WXX32O/6AaSpOigOOIEaNcLu5Ovsdv166Xu3z6V/igf5AGRYHFaAFvlZWWluZ7p6v3tRoPxo59PkPvbKUcmT9/vutR3tP1ZN/H92ZXL7PzzjvHOylnunr2qh6XXnqJD4peddX1LiBfKdsc/cF+6K961CtQn92kIPfy5ctdj+SP/Q1rNSJBgcnTT2/rg6zBuqrLgAH3u4aL3j7NzPjxL/t7JAQNI7nZVvXqB5p+wqdFixa54Otyu+CC8/xsbeeuu+51DU9jXWCzo78htFK7qFEi2qQv9JqiHdNo6wXzP/roE5fOZ5Dv7V+xYgXXs72CO2buQ2Sxon4R3bBXx2j48Med/d1+nka+3HPPQD+yRcFsTYk4ZiqnUKE033s7aGSZ6VLsKAB95JE19XKOU6tWp7jj1N+d7++bRtlMnfqhT3cT9H6PVkCkc1vv60cffczdUPh43zNePdSVwqpevR2NADm9rgaSzI2Ambd/2GGH+psTd+x4nn9Pjh37nAvY7EwnpvQ6Sm2kG/sqkKMe7aqDplNPbeFGiXT3o2YaNmzgA/caxXDjjddHHL0Qvu0PPpjiGxPUoKEbgc+e/YW74ff1fpGPP57hGjrq+tRfS5b8FFpNH3aiNf6FFnIPJk36wF0/Lgqf5R/ndI7kdN3o0qWzK7e7v/dAy5YtXOPNDN/4NHjw/b78eM+dLBVmBgIIIJBCAsWLF/P3purRo7vv0KHRXied1NB/nsrp+phf/2tSiI+q5iCgz0L9+9/jP6d2797Ffy5SascJ7p5KXbtenMPavIxA3gUaHHuUPfn8BJdyY5kdsF9Fe3fqLKt5WA0rW7qkL3TaJ1/YJpcqVj1zy7he/BXczX2nzfjC59kuUbyozfr8a1uzdoMdVG3H9/zj6tS0VydMs6mffG4nHV/bJk6e6T5jF7baR2YcUR7UuG2buqYfJgQQSD2BnN7vuhHvxA9mWGOXt7/yfhXsuGNq2ruTZ7nrw2xr3KCufbPwB/vJNRi2blY/w87PnrvA9i60V9TrRrBwkyr1TT9MCCCQVWCXB/2XLl3melTX8DVRr9caNar7x8Ev3fhWweoRIx73H3oVMFb+9QcfHOiDglpOAbEhQwbZww8/aoMGPeSDpw0a1HdB1QFBMfn+97LLuvo0LBq1oKC7AsnqxR0E6y644HyfTkY9v1T/888/1/cAU8UUwNWNVnWz2dGjn/R1VXqj1q1P9Y/1eosWzd06F7pURbVdzu/Rfn7mX5df3s03NNSv39j3nFe6mCA4rFzhSrNz3nnn+nsPKI2KGh+Us1wpQTSSQsH3MWNGukaW3i6VzBMu5/8Wn7Ll5ptvyLypqM91r4OePW9yvYCPdT3VH/EBzswLK4e+RhzoxsXKDa/GDPUc7tHjat9IUqNGdb+KUrwcfPBB7stzM98QokDpPffc6V/Tr9xsK7RwNg/UkDJ8+MMuaH2Xu7nwUD+aQA1M3bt3jbqW6pXdMY264r8vnHpqc9czfq7LA9zO519XGoDevW/xN4YN1lVv/+eee8H18D/Bz1KDUq9efd1okqbBIgk5ZirstNNa2TvvTHR5+Jv592CTJo182qjQhnJ4cMYZ7VyD0jf+2Ovc13l//fXX+Aa67FaNdG7fcsuN/kbBuhGwevmr0WbEiIf9PQZUVk6v670zbNij/j2o90SkqWfPa1zj1w1uH1v6ewvo3gk1ax4RWvSqq3q4NGN9nUdTX45GmATXk3r16rjz8C43Yqa/b+BSD8+LLurkG61CBUR5cM01V7og/60uMLTab1c3gg7Sj61Zs8YH7hW8D5/UcPLxx9mPktB7WD1Gg9ER4evn9L7O7rqhchRw0E3E1TCmuus9e+utN4XSr8V77oTXlccIIIBAqgm0bHmq7yTRqFFzf/+aOnVq23337RhlmdP1Mb/+16SaIfWNLqDRe2PGjLK+fe90nQ7a+tGhpUuX8gH/K6+8LPqKvIJAnALH1DrUmjf6zR5yN+fdtnW7u2Hm/nZppzNCpX69cLFL8bghlI7jii7n2NPj37Fedw33Hd7UG77Dmc3tiEOr+3XUMNDtwtPt2ZfftfGvfWBly5SyHhef6T5n7/LwQ2gfeIAAAvkjkNP7fZ2L702fMccF7//jg/5lSuv60M5efHOKvfLWNNcgmGZntD45S3B/3oLFdnC1ylY4jetG/hw5St0TBPZyQfUdOSpy2Ntlv6+yKpXK5rBUYl9WuhcF3xR0ijYpgKwe4Oq9WhCT+HTT1/D7CwT1UM7ubdu2+htfBvMy/1X9lRIo6Eke/roCoBoSpJ/sphUrVvqeyeHLKSioAPO4cU+HblirMh57bLTrbf+qS2/zVoYi169f73t/q9d0XiY1MmQejhWUc/31N/le5boBWTDppsIKsPbrd5u/Gap6Xqu3/ZQp7/oUQxoJEfQKD9YJ/ma3rWCZ3P6Vv86vYDRBTuvl5phmV4bqrnzpFVyu9/Djld060V6L95ipXO2/ei0qcJ+XSY05asDQKIVYpkjntmzUOBXt/Z7d6zqf9B7K6Tqguso92jZ0fDdt2pihMSZ8v5Smq1Sp0lHP9fBlg8e6Rug9GqtRsH48f7M7RyJdNzJvSyN0KlSoEPFcjffcybwtniOAAAKpJLBx40bfUB3ps0pO18f8+F+TSnbUNXcCShWlc0WfGZkQSJRATt/p9Rl90+atVrxY7r4T6gbAm93y6u0fbVrnbvZb0n3f2J2mnBx3p31lXxAIBHJz3sf6flcKn9xeb4J6pPrf3Dim+j5S/+QRSOqgf/IwpV5N9IHtqqt6upv9zvO54TXiQCMrpk//0I0wuNWnLNlVe6XUMVdf3dPdbLim6X4MCjDrJqRFiqS7Ht3DfIqb8KD/rqoX20EAAQQQQAABBBBAAAEE9hQBgk2JOdI4JsaRUlJLgPM+MccLx8Q4UkruBAj6584pZZdSz+kFCxaa0iqVK1fO9fqvne3Ig/zaUfV2Vh0WLPjWD4evUaOa6aa7waQbnup+D0Hqk2A+fxFAAAEEEEAAAQQQQAABBOIX+PmP1XZAxdIRR3HGX/qeUYK+1/6y3N17b98ye8YOs5cI/CvA9SP+U4HrR/yGlBCbAEH/2LxYGgEEEEAAAQQQQAABBBBAAIGUE/hr1Xor4m6oW7J4esrVPVkqvG7DFtu8ZauVL1siWapEPRDYJQJcP+Jn5voRvyElxCawd2yLszQCCCCAAAIIIIAAAggggAACCKSaQKkSRW31uo2mwJN6nDLlXkBecpOfHJkQ2NMEuH7k/Yhz/ci7HWvGJ0BP//j8WBsBBBBAAAEEEEAAAQQQQACBlBDYsnW7rV2/yTZt2UbgP4Yjttdee1nR9DQf8E8vXCiGNVkUgd1HgOtH3o4l14+8ubFW/AJp8RdBCQgggAACCCCAAAIIIIAAAgggkOwCCliTmibZjxL1QyA5Bbh+JOdxoVYIRBMgvU80GeYjgAACCCCAAAIIIIAAAggggAACCCCAAAIIIJBiAgT9U+yAUV0EEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBKIJEPSPJsN8BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQRSTICgf4odMKqLAAIIIIAAAggggAACCCCAAAIIIIAAAggggEA0AYL+0WSYjwACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAigkQ9E+xA0Z1EUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBCIJkDQP5oM8xFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQSDEBgv4pdsCoLgIIIIAAAggggAACCCCAAAIIIIAAAggggAAC0QQI+keTYT4CCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAikmEFPQ/58U2zmqiwACCCCAAAIIIIAAAggggAACCCCAAAIIIIDAniSQ66B/WqG9bdu27XuSDfuKAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBKCeQ66F8kvbBt2rwtpXaOyiKAAAIIIIAAAggggAACCCCAAAIIIIAAAgggsCcJ5DroX6JYuq3buNlI8bMnnR7sKwIIIIAAAggggAACCCCAAAIIIIAAAggggEAqCeQ66J9euJAVdb39V63ZmEr7R10RQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEENhjBHId9JdIudLFbPv27bbSBf7p8b/HnCPsKAIIIIAAAggggAACCCCAAAIIIIAAAggggECKCOz1j5tirauC/pu2bLWSxYpY0SJplpZWyPaKtRCWRwABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgYQK5Cnorxps2brd1m/cYptd8H/b9r8TWikKQwABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgdgF8hz0j31TrIEAAggggAACCCCAAAIIIIAAAggggAACCCCAAAL5KRBTTv/8rAhlI4AAAggggAACCCCAAAIIIIAAAggggAACCCCAQHwCBP3j82NtBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSSRoCgf9IcCiqCAAIIIIAAAggggAACCCCAAAIIIIAAAggggEB8AgT94/NjbQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEkkaAoH/SHAoqggACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAfAIE/ePzY20EEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBJJGgKB/0hwKKoIAAggggAACCCCAAAIIIIAAAggggAACCCCAQHwCBP3j82NtBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSSRuD/AWxXftx7Cf01AAAAAElFTkSuQmCC"
+ }
+ },
+ "cell_type": "markdown",
+ "id": "47c38cd3-2c31-48c4-8281-bd9e1f7b2830",
+ "metadata": {},
+ "source": [
+ "We can see the results benchmarked against `GPT-4o` and `Llama-3-70b` using `Custom` agent (as shown here) and ReAct.\n",
+ "\n",
+ "\n",
+ "\n",
+ "The `local custom agent` performs well in terms of tool calling reliability: it follows the expected reasoning traces.\n",
+ "\n",
+ "However, the answer accuracy performance lags the larger models with `custom agent` implementations."
+ ]
+ }
+ ],
+ "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.8"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/langgraph/source/examples/rag/langgraph_self_rag.ipynb b/langgraph/source/examples/rag/langgraph_self_rag.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..23751308ce25e9dfe0d1e6e7aa8f10e2e007605f
--- /dev/null
+++ b/langgraph/source/examples/rag/langgraph_self_rag.ipynb
@@ -0,0 +1,800 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "b3d959ff",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
+ ]
+ },
+ {
+ "attachments": {
+ "15cba0ab-a549-4909-8373-fb761e384eff.png": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAABpwAAAJ0CAYAAAAPhYDIAAAMP2lDQ1BJQ0MgUHJvZmlsZQAASImVVwdYU8kWnluSkEBCCSAgJfQmCEgJICWEFkB6EWyEJEAoMQaCiB1dVHDtYgEbuiqi2AGxI3YWwd4XRRSUdbFgV96kgK77yvfO9829//3nzH/OnDu3DADqp7hicQ6qAUCuKF8SGxLAGJucwiB1AwTggAYIgMDl5YlZ0dERANrg+e/27ib0hnbNQab1z/7/app8QR4PACQa4jR+Hi8X4kMA4JU8sSQfAKKMN5+aL5Zh2IC2BCYI8UIZzlDgShlOU+B9cp/4WDbEzQCoqHG5kgwAaG2QZxTwMqAGrQ9iJxFfKAJAnQGxb27uZD7EqRDbQB8xxDJ9ZtoPOhl/00wb0uRyM4awYi5yUwkU5olzuNP+z3L8b8vNkQ7GsIJNLVMSGiubM6zb7ezJ4TKsBnGvKC0yCmItiD8I+XJ/iFFKpjQ0QeGPGvLy2LBmQBdiJz43MBxiQ4iDRTmREUo+LV0YzIEYrhC0UJjPiYdYD+KFgrygOKXPZsnkWGUstC5dwmYp+QtciTyuLNZDaXYCS6n/OlPAUepjtKLM+CSIKRBbFAgTIyGmQeyYlx0XrvQZXZTJjhz0kUhjZflbQBwrEIUEKPSxgnRJcKzSvzQ3b3C+2OZMISdSiQ/kZ8aHKuqDNfO48vzhXLA2gYiVMKgjyBsbMTgXviAwSDF3rFsgSohT6nwQ5wfEKsbiFHFOtNIfNxPkhMh4M4hd8wrilGPxxHy4IBX6eLo4PzpekSdelMUNi1bkgy8DEYANAgEDSGFLA5NBFhC29tb3witFTzDgAgnIAALgoGQGRyTJe0TwGAeKwJ8QCUDe0LgAea8AFED+6xCrODqAdHlvgXxENngKcS4IBznwWiofJRqKlgieQEb4j+hc2Hgw3xzYZP3/nh9kvzMsyEQoGelgRIb6oCcxiBhIDCUGE21xA9wX98Yj4NEfNheciXsOzuO7P+EpoZ3wmHCD0EG4M0lYLPkpyzGgA+oHK2uR9mMtcCuo6YYH4D5QHSrjurgBcMBdYRwW7gcju0GWrcxbVhXGT9p/m8EPd0PpR3Yio+RhZH+yzc8jaXY0tyEVWa1/rI8i17SherOHen6Oz/6h+nx4Dv/ZE1uIHcTOY6exi9gxrB4wsJNYA9aCHZfhodX1RL66BqPFyvPJhjrCf8QbvLOySuY51Tj1OH1R9OULCmXvaMCeLJ4mEWZk5jNY8IsgYHBEPMcRDBcnF1cAZN8XxevrTYz8u4Hotnzn5v0BgM/JgYGBo9+5sJMA7PeAj/+R75wNE346VAG4cIQnlRQoOFx2IMC3hDp80vSBMTAHNnA+LsAdeAN/EATCQBSIB8lgIsw+E65zCZgKZoC5oASUgWVgNVgPNoGtYCfYAw6AenAMnAbnwGXQBm6Ae3D1dIEXoA+8A58RBCEhVISO6CMmiCVij7ggTMQXCUIikFgkGUlFMhARIkVmIPOQMmQFsh7ZglQj+5EjyGnkItKO3EEeIT3Ia+QTiqFqqDZqhFqhI1EmykLD0Xh0ApqBTkGL0PnoEnQtWoXuRuvQ0+hl9Abagb5A+zGAqWK6mCnmgDExNhaFpWDpmASbhZVi5VgVVos1wvt8DevAerGPOBGn4wzcAa7gUDwB5+FT8Fn4Ynw9vhOvw5vxa/gjvA//RqASDAn2BC8ChzCWkEGYSighlBO2Ew4TzsJnqYvwjkgk6hKtiR7wWUwmZhGnExcTNxD3Ek8R24mdxH4SiaRPsif5kKJIXFI+qYS0jrSbdJJ0ldRF+qCiqmKi4qISrJKiIlIpVilX2aVyQuWqyjOVz2QNsiXZixxF5pOnkZeSt5EbyVfIXeTPFE2KNcWHEk/JosylrKXUUs5S7lPeqKqqmql6qsaoClXnqK5V3ad6QfWR6kc1LTU7NbbaeDWp2hK1HWqn1O6ovaFSqVZUf2oKNZ+6hFpNPUN9SP1Ao9McaRwanzabVkGro12lvVQnq1uqs9Qnqhepl6sfVL+i3qtB1rDSYGtwNWZpVGgc0bil0a9J13TWjNLM1VysuUvzoma3FknLSitIi681X2ur1hmtTjpGN6ez6Tz6PPo2+ll6lzZR21qbo52lXaa9R7tVu09HS8dVJ1GnUKdC57hOhy6ma6XL0c3RXap7QPem7qdhRsNYwwTDFg2rHXZ12Hu94Xr+egK9Ur29ejf0Pukz9IP0s/WX69frPzDADewMYgymGmw0OGvQO1x7uPdw3vDS4QeG3zVEDe0MYw2nG241bDHsNzI2CjESG60zOmPUa6xr7G+cZbzK+IRxjwndxNdEaLLK5KTJc4YOg8XIYaxlNDP6TA1NQ02lpltMW00/m1mbJZgVm+01e2BOMWeap5uvMm8y77MwsRhjMcOixuKuJdmSaZlpucbyvOV7K2urJKsFVvVW3dZ61hzrIusa6/s2VBs/myk2VTbXbYm2TNts2w22bXaonZtdpl2F3RV71N7dXmi/wb59BGGE5wjRiKoRtxzUHFgOBQ41Do8cdR0jHIsd6x1fjrQYmTJy+cjzI785uTnlOG1zuues5RzmXOzc6Pzaxc6F51Lhcn0UdVTwqNmjGka9crV3FbhudL3tRncb47bArcntq7uHu8S91r3Hw8Ij1aPS4xZTmxnNXMy84EnwDPCc7XnM86OXu1e+1wGvv7wdvLO9d3l3j7YeLRi9bXSnj5kP12eLT4cvwzfVd7Nvh5+pH9evyu+xv7k/33+7/zOWLSuLtZv1MsApQBJwOOA924s9k30qEAsMCSwNbA3SCkoIWh/0MNgsOCO4JrgvxC1kesipUEJoeOjy0FscIw6PU83pC/MImxnWHK4WHhe+PvxxhF2EJKJxDDombMzKMfcjLSNFkfVRIIoTtTLqQbR19JToozHEmOiYipinsc6xM2LPx9HjJsXtinsXHxC/NP5egk2CNKEpUT1xfGJ14vukwKQVSR1jR46dOfZyskGyMLkhhZSSmLI9pX9c0LjV47rGu40vGX9zgvWEwgkXJxpMzJl4fJL6JO6kg6mE1KTUXalfuFHcKm5/GietMq2Px+at4b3g+/NX8XsEPoIVgmfpPukr0rszfDJWZvRk+mWWZ/YK2cL1wldZoVmbst5nR2XvyB7IScrZm6uSm5p7RKQlyhY1TzaeXDi5XWwvLhF3TPGasnpKnyRcsj0PyZuQ15CvDX/kW6Q20l+kjwp8CyoKPkxNnHqwULNQVNgyzW7aomnPioKLfpuOT+dNb5phOmPujEczWTO3zEJmpc1qmm0+e/7srjkhc3bOpczNnvt7sVPxiuK385LmNc43mj9nfucvIb/UlNBKJCW3Fngv2LQQXyhc2Lpo1KJ1i76V8ksvlTmVlZd9WcxbfOlX51/X/jqwJH1J61L3pRuXEZeJlt1c7rd85wrNFUUrOleOWVm3irGqdNXb1ZNWXyx3Ld+0hrJGuqZjbcTahnUW65at+7I+c/2NioCKvZWGlYsq32/gb7i60X9j7SajTWWbPm0Wbr69JWRLXZVVVflW4taCrU+3JW47/xvzt+rtBtvLtn/dIdrRsTN2Z3O1R3X1LsNdS2vQGmlNz+7xu9v2BO5pqHWo3bJXd2/ZPrBPuu/5/tT9Nw+EH2g6yDxYe8jyUOVh+uHSOqRuWl1ffWZ9R0NyQ/uRsCNNjd6Nh486Ht1xzPRYxXGd40tPUE7MPzFwsuhk/ynxqd7TGac7myY13Tsz9sz15pjm1rPhZy+cCz535jzr/MkLPheOXfS6eOQS81L9ZffLdS1uLYd/d/v9cKt7a90VjysNbZ5tje2j209c9bt6+lrgtXPXOdcv34i80X4z4ebtW+Nvddzm3+6+k3Pn1d2Cu5/vzblPuF/6QONB+UPDh1V/2P6xt8O94/ijwEctj+Me3+vkdb54kvfkS9f8p9Sn5c9MnlV3u3Qf6wnuaXs+7nnXC/GLz70lf2r+WfnS5uWhv/z/aukb29f1SvJq4PXiN/pvdrx1fdvUH93/8F3uu8/vSz/of9j5kfnx/KekT88+T/1C+rL2q+3Xxm/h3+4P5A4MiLkSrvxXAIMNTU8H4PUOAKjJANDh/owyTrH/kxui2LPKEfhPWLFHlJs7ALXw/z2mF/7d3AJg3za4/YL66uMBiKYCEO8J0FGjhtrgXk2+r5QZEe4DNkd+TctNA//GFHvOH/L++Qxkqq7g5/O/AFFLfCfKufu9AAAAVmVYSWZNTQAqAAAACAABh2kABAAAAAEAAAAaAAAAAAADkoYABwAAABIAAABEoAIABAAAAAEAAAacoAMABAAAAAEAAAJ0AAAAAEFTQ0lJAAAAU2NyZWVuc2hvdHAfBRUAAAHXaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjYyODwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4xNjkyPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6VXNlckNvbW1lbnQ+U2NyZWVuc2hvdDwvZXhpZjpVc2VyQ29tbWVudD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cr1F+NQAAEAASURBVHgB7N0HeBTl2sbxhw4CoUqRDkq1gxRFBAVRwYYiVj4rKorHgr2gWLDgsaBYsKAiCtiPKIgIilIVxYIiKL1K71W+uV+ccXazCUk2IZvN/72uzU7fmd8sOce587xvgd1eMxoCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACWRQomMX92A0BBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABJ0DgxBcBAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAgLgECp7j42BkBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQIDAie8AAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAXAIETnHxsTMCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggACBE98BBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBuAQInOLiY2cEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAECJ74DCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACcQkQOMXFx84IIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIETnwHEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEE4hIgcIqLj50RQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQInPgOIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIxCVA4BQXHzsjgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggQOPEdQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQiEuAwCkuPnZGAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAgcOI7gAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEJcAgVNcfOyMAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCBA4MR3AAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIC4BAqe4+NgZAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEECAwInvAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQFwCBE5x8bEzAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAgRPfAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgbgECJzi4mNnBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABAie+AwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAnEJEDjFxcfOCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACBE58BxBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBOISIHCKi4+dEUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEECJz4DiCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCMQlQOAUFx87I4AAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIEDjxHUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEIhLgMApLj52RgABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQIHDiO4AAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBCXAIFTXHzsjAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggQODEdwABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQCAuAQKnuPjYGQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAgMCJ7wACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBcAgROcfGxMwIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAIET3wEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIG4BAic4uJjZwQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQInvgMIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAJxCRA4xcXHzggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgROfAcQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQTiEiBwiouPnRFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBAic+A4ggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgjEJUDgFBcfOyOAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCBA48R1AAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBCIS4DAKS4+dkYAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEECBw4juAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCAQlwCBU1x87IwAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIEDgxHcAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAgLgECp7j42BkBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQIDAie8AAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAXAIETnHxsTMCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggACBE98BBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBuAQInOLiY2cEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAECJ74DCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACcQkQOMXFx84IIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIETnwHEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEE4hIgcIqLj50RQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQInPgOIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIxCVA4BQXHzsjgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggQOPEdQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQiEuAwCkuPnZGAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAgcOI7gAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEJcAgVNcfOyMAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCBA4MR3AAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIC6BwnHtzc4IIIAAAggggAACCCCA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vRYuXGi7du2yihUrWq1atax27dqmqqb82gic8uud57oRQAABBBBAAAEEEEAAAQQQQAABBBBAIMcFVBHx+uuv27Rp09xn5ZexXHIclg9AAIGEEyBwSrhbwgkhgAACCCCAAAIIIIAAAggggAACCCCAQF4WWLFihQ0fPtzef/99mzNnTsSldO/e3e6///6IZcwggAACySCQfzoPTIa7xTUggAACCCCAAAIIIIAAAggggAACCCCAQMIKKGgaOnSoey1fvtydZ+XKlc2f1oIKFSok7PlzYggggEA8AgRO8eixLwIIIIAAAggggAACCCCAAAIIIIAAAgjke4HooKlAgQLWq1cvmzBhgv3www+BT48ePez6668P5plAAAEEkkmALvWS6W5yLQgggAACCCCAAAIIIIAAAggggAACCCCwTwWefPLJoKKpZMmSLmg699xz7aqrrrLJkycH59KwYUMbPXp0MM8EAgggkGwCBE7Jdke5HgQQQAABBBBAAAEEEEAAAQQQQAABBBDIUYGtW7fa4MGD7dVXX7Vly5ZZpUqVXNCk8ZnUunXrFhE2aZnGczryyCM1SUMAAQSSUoAu9ZLytnJRCCCAAAIIIIAAAggggAACCCCAAAIIIJATAm+//bYLm3799VcrX768PfbYY3bOOecEH3XdddelCptOPvlkwqZAiAkEEEhWAQKnZL2zXBcCCCCAAAIIIIAAAggggAACCCCAAAIIZJvAqFGjXNA0adIkK1y4sKto6t27d8TxH3roIfvwww8jlmnmrLPOSrWMBQgggECyCRA4Jdsd5XoQQAABBBBAAAEEEEAAAQQQQAABBBBAINsENA6Tus5T4KSm7vJuuukmq1y5csRnaJsXXnghWKZu9lasWGEtWrSwDh06BMuZQAABBJJVgMApWe8s14UAAggggAACCCCAAAIIIIAAAggggAACWRb46aefbMiQIaYu9NROOeUUu/jii12AFH3Q0aNH27333hssrlu3rv35559unuqmgIUJBBBIcgECpyS/wVweAggggAACCCCAAAIIIIAAAggggAACCGRcYOnSpfbSSy+5l/Y64YQT7Pzzz7f27dvHPMi0adOsR48ewbo6derYoYce6gKn+vXr29lnnx2sYwIBBBBIZgECp2S+u1wbAggggAACCCCAAAIIIIAAAggggAACCGRK4KKLLrLZs2dbq1atXEXTSSedlOb+M2fOjAiU1I1ev3797LLLLnP7KGwqVKhQmvuzAgEEEEgmAQKnZLqbXAsCCCCAAAIIIIAAAggggAACCCCAAAIIxCVw5ZVX2q5du+zcc89N9zhz5861k08+OdimVKlSNmjQIPvuu+9s06ZNbownqpsCHiYQQCAfCBA45YObzCUigAACCCCAAAIIIIAAAggggAACCCCAQMYEunbtutcNly9fbm3btg22K1iwoAubDj/8cHvwwQfdcoVNFSpUCLZhAgEEEEh2gYLJfoFcHwIIIIAAAggggAACCCCAAAIIIIAAAgggkF0CGzdutObNm0cc7sUXX7Sjjz7apk6d6l7FixeP6GovYmNmEEAAgSQVIHBK0hvLZSGAAAIIIIAAAggggAACCCCAAAIIIIBA9gqoq70mTZpEHPTpp5+2Dh06uGUjR45076puqlu3bsR2zCCAAALJLkDglOx3mOtDAAEEEEAAAQQQQAABBBBAAAEEEEAAgWwRqF+/fsRx+vXrZ6effrpbtnPnThs1apSb9pdFbMwMAgggkOQCBE5JfoO5PAQQQAABBBBAAAEEEEAAAQQQQAABBBCIX+Coo44yhUp+u/POO+3888/3Z+1///ufLVu2zI3tFN3lXrAREwgggEASCxA4JfHN5dIQQAABBBBAAAEEEEAAAQQQQAABBBBAIH6BE0880VasWBEc6IYbbrAePXoE85r46KOP3Pxpp50WsZwZBBBAIL8IEDjllzvNdSKAAAIIIIAAAggggAACCCCAAAIIIIBApgW6detms2bNCvZT0HT99dcH85qYP3++ffHFF27cJgKnCBpmEEAgHwkQOOWjm82lIoAAAggggAACCCCAAAIIIIAAAggggEDGBa666iqbPHlysMOFF15o6kovuvnVTRq7qUiRItGrmUcAAQTyhQCBU764zVwkAggggAACCCCAAAIIIIAAAggggAACCGRG4I477rBPP/002OXMM8+0Bx98MJgPT2j8ppSUFDv77LPDi5lGAAEE8pUAgVO+ut1cLAIIIIAAAggggAACCCCAAAIIIIAAAgjsTaB///725ptvBptpDKcnn3wymA9PjB071nW517VrV6tevXp4FdMIIIBAvhIgcMpXt5uLRQABBBBAAAEEEEAAAQQQQAABBBBAAIH0BAYNGmQDBgwINjnmmGNMy9JqP/74o1tF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WRse/evbrdvQHW85l3qeValIgkbFJZ7PNHd6eux+ldn/PZo65saw6uHZrf1zGUSF48Xt+X7+wNTwi69PDjFAlVVG1lWFZ5fMah2jOJNpYFXfMP8TDcn7U8KggV1vQhEzqH72tabw1E8+FZrGDc5nNWX9bmw7cuJMh0cxaLd1T3N45qV3w4+Xvx4WPfpKOP3B+VS/h+66NtlkqUFVCBAgQIAAAQIECBC4bgX6XEzKdft1PowAAQIECBAgQIAAgbIWiNk53022n3t929HM75h6S2144ge3Zz7rrcaDxz8I7xw4FaqSANWtjTWhX9s2dlfye/F8pSPJOA3J1oEjkmBUruxNtrWLAanqJAg1oF9F6JMdv8p1z19PJIG37ftPhpOnLwXgYlCqceiAUF/TP3OM7W0ZTfkBim5+uXh6mDN5aFGrKgECBAgQIECAAAECBC4vIOB0eRtPCBAgQIAAAQIECBAoAYG45dzvX9gRlj+3o8NsbhpSFf6WZB8pVyaw9s3D4UePv9bhpeH1A8KyBz4WmkbVdHimgQABAgQIECBAgAABAp0JdL1fQ2dve0aAAAECBAgQIECAAIFeFogZP9/5wvhw98zGsGr93vDCvw+E/UmG0Kkkm6f5VNdb6vXy9Mpy+CPNl7bZi7Y11ZVhWnJe05c/OSLMaRrarXOkyvKjTZoAAQIECBAgQIAAgV4VkOHUq7wGJ0CAAAECBAgQIECgNwXiBuHd3XauN+dRjmOzK8dVM2cCBAgQIECAAAECpSsg4FS6a2NmBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGyELihLGZpkgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAiUrIOBUsktjYgQIECBAgAABAgQIECBAgAABAgQIECBAgACB8hAQcCqPdTJLAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDJCgg4lezSmBgBAgQIECBAgAABAgQIECBAgAABAgQIECBAoDwEBJzKY53MkgABAgQIECBAgAABAgQIECBAgAABAgQIECBQsgICTiW7NCZGgAABAgQIECBAgAABAgQIECBAgAABAgQIECgPAQGn8lgnsyRAgAABAgQIECBAgAABAgQIECBAgAABAgQIlKyAgFPJLo2JESBAgAABAgQIECBAgAABAgQIECBAgAABAgTKQ0DAqTzWySwJECBAgAABAgQIECBAgAABAgQIECBAgAABAiUrIOBUsktjYgQIECBAgAABAgQIECBAgAABAgQIECBAgACB8hAQcCqPdTJLAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDJCgg4lezSmBgBAgQIECBAgAABAgQIECBAgAABAgQIECBAoDwEBJzKY53MkgABAgQIECBAgAABAgQIECBAgAABAgQIECBQsgICTiW7NCZGgAABAgQIECBAgAABAgQIECBAgAABAgQIECgPAQGn8lgnsyRAgAABAgQIECBAgAABAgQIECBAgAABAgQIlKzA/wHZpDlUM4Q3oAAAAABJRU5ErkJggg=="
+ }
+ },
+ "cell_type": "markdown",
+ "id": "919fe33c-0149-4f7d-b200-544a18986c9a",
+ "metadata": {},
+ "source": [
+ "# Self-RAG\n",
+ "\n",
+ "Self-RAG is a strategy for RAG that incorporates self-reflection / self-grading on retrieved documents and generations. \n",
+ "\n",
+ "In the [paper](https://arxiv.org/abs/2310.11511), a few decisions are made:\n",
+ "\n",
+ "1. Should I retrieve from retriever, `R` -\n",
+ "\n",
+ "* Input: `x (question)` OR `x (question)`, `y (generation)`\n",
+ "* Decides when to retrieve `D` chunks with `R`\n",
+ "* Output: `yes, no, continue`\n",
+ "\n",
+ "2. Are the retrieved passages `D` relevant to the question `x` -\n",
+ "\n",
+ "* * Input: (`x (question)`, `d (chunk)`) for `d` in `D`\n",
+ "* `d` provides useful information to solve `x`\n",
+ "* Output: `relevant, irrelevant`\n",
+ "\n",
+ "3. Are the LLM generation from each chunk in `D` is relevant to the chunk (hallucinations, etc) -\n",
+ "\n",
+ "* Input: `x (question)`, `d (chunk)`, `y (generation)` for `d` in `D`\n",
+ "* All of the verification-worthy statements in `y (generation)` are supported by `d`\n",
+ "* Output: `{fully supported, partially supported, no support`\n",
+ "\n",
+ "4. The LLM generation from each chunk in `D` is a useful response to `x (question)` -\n",
+ "\n",
+ "* Input: `x (question)`, `y (generation)` for `d` in `D`\n",
+ "* `y (generation)` is a useful response to `x (question)`.\n",
+ "* Output: `{5, 4, 3, 2, 1}`\n",
+ "\n",
+ "We will implement some of these ideas from scratch using [LangGraph](https://langchain-ai.github.io/langgraph/).\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "72f3ee57-68ab-4040-bd36-4014e2a23d96",
+ "metadata": {},
+ "source": [
+ "## Setup\n",
+ "\n",
+ "First let's install our required packages and set our API keys"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a384cc48-0425-4e8f-aafc-cfb8e56025c9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "! pip install -U langchain_community tiktoken langchain-openai langchainhub chromadb langchain langgraph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "de4ee2a5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import getpass\n",
+ "import os\n",
+ "\n",
+ "\n",
+ "def _set_env(key: str):\n",
+ " if key not in os.environ:\n",
+ " os.environ[key] = getpass.getpass(f\"{key}:\")\n",
+ "\n",
+ "\n",
+ "_set_env(\"OPENAI_API_KEY\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "25d16369",
+ "metadata": {},
+ "source": [
+ "
\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": "c27bebdc-be71-4130-ab9d-42f09f87658b",
+ "metadata": {},
+ "source": [
+ "## Retriever\n",
+ " \n",
+ "Let's index 3 blog posts."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "565a6d44-2c9f-4fff-b1ec-eea05df9350d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
+ "from langchain_community.document_loaders import WebBaseLoader\n",
+ "from langchain_community.vectorstores import Chroma\n",
+ "from langchain_openai import OpenAIEmbeddings\n",
+ "\n",
+ "urls = [\n",
+ " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
+ "]\n",
+ "\n",
+ "docs = [WebBaseLoader(url).load() for url in urls]\n",
+ "docs_list = [item for sublist in docs for item in sublist]\n",
+ "\n",
+ "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
+ " chunk_size=250, chunk_overlap=0\n",
+ ")\n",
+ "doc_splits = text_splitter.split_documents(docs_list)\n",
+ "\n",
+ "# Add to vectorDB\n",
+ "vectorstore = Chroma.from_documents(\n",
+ " documents=doc_splits,\n",
+ " collection_name=\"rag-chroma\",\n",
+ " embedding=OpenAIEmbeddings(),\n",
+ ")\n",
+ "retriever = vectorstore.as_retriever()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "29c12f74-53e2-43cc-896f-875d1c5d9d93",
+ "metadata": {},
+ "source": [
+ "## LLMs"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "1fafad21-60cc-483e-92a3-6a7edb1838e3",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/rlm/miniforge3/envs/llama2/lib/python3.11/site-packages/langchain_core/_api/deprecation.py:119: LangChainDeprecationWarning: The method `BaseRetriever.get_relevant_documents` was deprecated in langchain-core 0.1.46 and will be removed in 0.3.0. Use invoke instead.\n",
+ " warn_deprecated(\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "binary_score='yes'\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Retrieval Grader\n",
+ "\n",
+ "\n",
+ "from langchain_core.prompts import ChatPromptTemplate\n",
+ "from langchain_core.pydantic_v1 import BaseModel, Field\n",
+ "from langchain_openai import ChatOpenAI\n",
+ "\n",
+ "\n",
+ "# Data model\n",
+ "class GradeDocuments(BaseModel):\n",
+ " \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n",
+ "\n",
+ " binary_score: str = Field(\n",
+ " description=\"Documents are relevant to the question, 'yes' or 'no'\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# LLM with function call\n",
+ "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
+ "structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
+ "\n",
+ "# Prompt\n",
+ "system = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
+ " It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n",
+ " If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n",
+ " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n",
+ "grade_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"system\", system),\n",
+ " (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "retrieval_grader = grade_prompt | structured_llm_grader\n",
+ "question = \"agent memory\"\n",
+ "docs = retriever.get_relevant_documents(question)\n",
+ "doc_txt = docs[1].page_content\n",
+ "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "dcd77cc1-4587-40ec-b633-5364eab9e1ec",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The design of generative agents combines LLM with memory, planning, and reflection mechanisms to enable agents to behave conditioned on past experience and interact with other agents. Long-term memory provides the agent with the capability to retain and recall infinite information over extended periods. Short-term memory is utilized for in-context learning.\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Generate\n",
+ "\n",
+ "from langchain import hub\n",
+ "from langchain_core.output_parsers import StrOutputParser\n",
+ "\n",
+ "# Prompt\n",
+ "prompt = hub.pull(\"rlm/rag-prompt\")\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n",
+ "\n",
+ "\n",
+ "# Post-processing\n",
+ "def format_docs(docs):\n",
+ " return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
+ "\n",
+ "\n",
+ "# Chain\n",
+ "rag_chain = prompt | llm | StrOutputParser()\n",
+ "\n",
+ "# Run\n",
+ "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
+ "print(generation)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "e78931ec-940c-46ad-a0b2-f43f953f1fd7",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "GradeHallucinations(binary_score='yes')"
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Hallucination Grader\n",
+ "\n",
+ "\n",
+ "# Data model\n",
+ "class GradeHallucinations(BaseModel):\n",
+ " \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n",
+ "\n",
+ " binary_score: str = Field(\n",
+ " description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# LLM with function call\n",
+ "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
+ "structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n",
+ "\n",
+ "# Prompt\n",
+ "system = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n",
+ " Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\n",
+ "hallucination_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"system\", system),\n",
+ " (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "hallucination_grader = hallucination_prompt | structured_llm_grader\n",
+ "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "bd62276f-bf26-40d0-8cff-e07b10e00321",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "GradeAnswer(binary_score='yes')"
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Answer Grader\n",
+ "\n",
+ "\n",
+ "# Data model\n",
+ "class GradeAnswer(BaseModel):\n",
+ " \"\"\"Binary score to assess answer addresses question.\"\"\"\n",
+ "\n",
+ " binary_score: str = Field(\n",
+ " description=\"Answer addresses the question, 'yes' or 'no'\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# LLM with function call\n",
+ "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
+ "structured_llm_grader = llm.with_structured_output(GradeAnswer)\n",
+ "\n",
+ "# Prompt\n",
+ "system = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n",
+ " Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\n",
+ "answer_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"system\", system),\n",
+ " (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "answer_grader = answer_prompt | structured_llm_grader\n",
+ "answer_grader.invoke({\"question\": question, \"generation\": generation})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "c6f4c70e-1660-4149-82c0-837f19fc9fb5",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "\"What is the role of memory in an agent's functioning?\""
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Question Re-writer\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
+ "\n",
+ "# Prompt\n",
+ "system = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n",
+ " for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\n",
+ "re_write_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"system\", system),\n",
+ " (\n",
+ " \"human\",\n",
+ " \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n",
+ " ),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "question_rewriter = re_write_prompt | llm | StrOutputParser()\n",
+ "question_rewriter.invoke({\"question\": question})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "276001c5-c079-4e5b-9f42-81a06704d200",
+ "metadata": {},
+ "source": [
+ "# Graph \n",
+ "\n",
+ "Capture the flow in as a graph.\n",
+ "\n",
+ "## Graph state"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "f1617e9e-66a8-4c1a-a1fe-cc936284c085",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from typing import List\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "\n",
+ "class GraphState(TypedDict):\n",
+ " \"\"\"\n",
+ " Represents the state of our graph.\n",
+ "\n",
+ " Attributes:\n",
+ " question: question\n",
+ " generation: LLM generation\n",
+ " documents: list of documents\n",
+ " \"\"\"\n",
+ "\n",
+ " question: str\n",
+ " generation: str\n",
+ " documents: List[str]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "add509d8-6682-4127-8d95-13dd37d79702",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "### Nodes\n",
+ "\n",
+ "\n",
+ "def retrieve(state):\n",
+ " \"\"\"\n",
+ " Retrieve documents\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, documents, that contains retrieved documents\n",
+ " \"\"\"\n",
+ " print(\"---RETRIEVE---\")\n",
+ " question = state[\"question\"]\n",
+ "\n",
+ " # Retrieval\n",
+ " documents = retriever.get_relevant_documents(question)\n",
+ " return {\"documents\": documents, \"question\": question}\n",
+ "\n",
+ "\n",
+ "def generate(state):\n",
+ " \"\"\"\n",
+ " Generate answer\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, generation, that contains LLM generation\n",
+ " \"\"\"\n",
+ " print(\"---GENERATE---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # RAG generation\n",
+ " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
+ " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
+ "\n",
+ "\n",
+ "def grade_documents(state):\n",
+ " \"\"\"\n",
+ " Determines whether the retrieved documents are relevant to the question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates documents key with only filtered relevant documents\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # Score each doc\n",
+ " filtered_docs = []\n",
+ " for d in documents:\n",
+ " score = retrieval_grader.invoke(\n",
+ " {\"question\": question, \"document\": d.page_content}\n",
+ " )\n",
+ " grade = score.binary_score\n",
+ " if grade == \"yes\":\n",
+ " print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
+ " filtered_docs.append(d)\n",
+ " else:\n",
+ " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n",
+ " continue\n",
+ " return {\"documents\": filtered_docs, \"question\": question}\n",
+ "\n",
+ "\n",
+ "def transform_query(state):\n",
+ " \"\"\"\n",
+ " Transform the query to produce a better question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates question key with a re-phrased question\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---TRANSFORM QUERY---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # Re-write question\n",
+ " better_question = question_rewriter.invoke({\"question\": question})\n",
+ " return {\"documents\": documents, \"question\": better_question}\n",
+ "\n",
+ "\n",
+ "### Edges\n",
+ "\n",
+ "\n",
+ "def decide_to_generate(state):\n",
+ " \"\"\"\n",
+ " Determines whether to generate an answer, or re-generate a question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Binary decision for next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---ASSESS GRADED DOCUMENTS---\")\n",
+ " state[\"question\"]\n",
+ " filtered_documents = state[\"documents\"]\n",
+ "\n",
+ " if not filtered_documents:\n",
+ " # All documents have been filtered check_relevance\n",
+ " # We will re-generate a new query\n",
+ " print(\n",
+ " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
+ " )\n",
+ " return \"transform_query\"\n",
+ " else:\n",
+ " # We have relevant documents, so generate answer\n",
+ " print(\"---DECISION: GENERATE---\")\n",
+ " return \"generate\"\n",
+ "\n",
+ "\n",
+ "def grade_generation_v_documents_and_question(state):\n",
+ " \"\"\"\n",
+ " Determines whether the generation is grounded in the document and answers question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Decision for next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---CHECK HALLUCINATIONS---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ " generation = state[\"generation\"]\n",
+ "\n",
+ " score = hallucination_grader.invoke(\n",
+ " {\"documents\": documents, \"generation\": generation}\n",
+ " )\n",
+ " grade = score.binary_score\n",
+ "\n",
+ " # Check hallucination\n",
+ " if grade == \"yes\":\n",
+ " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n",
+ " # Check question-answering\n",
+ " print(\"---GRADE GENERATION vs QUESTION---\")\n",
+ " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
+ " grade = score.binary_score\n",
+ " if grade == \"yes\":\n",
+ " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
+ " return \"useful\"\n",
+ " else:\n",
+ " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n",
+ " return \"not useful\"\n",
+ " else:\n",
+ " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n",
+ " return \"not supported\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "61cd5797-1782-4d78-a277-8196d13f3e1b",
+ "metadata": {},
+ "source": [
+ "## Build Graph\n",
+ "\n",
+ "The just follows the flow we outlined in the figure above."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "0e09ca9f-e36d-4ef4-a0d5-79fdbada9fe0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langgraph.graph import END, StateGraph, START\n",
+ "\n",
+ "workflow = StateGraph(GraphState)\n",
+ "\n",
+ "# Define the nodes\n",
+ "workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
+ "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
+ "workflow.add_node(\"generate\", generate) # generate\n",
+ "workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
+ "\n",
+ "# Build graph\n",
+ "workflow.add_edge(START, \"retrieve\")\n",
+ "workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"grade_documents\",\n",
+ " decide_to_generate,\n",
+ " {\n",
+ " \"transform_query\": \"transform_query\",\n",
+ " \"generate\": \"generate\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"transform_query\", \"retrieve\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"generate\",\n",
+ " grade_generation_v_documents_and_question,\n",
+ " {\n",
+ " \"not supported\": \"generate\",\n",
+ " \"useful\": END,\n",
+ " \"not useful\": \"transform_query\",\n",
+ " },\n",
+ ")\n",
+ "\n",
+ "# Compile\n",
+ "app = workflow.compile()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "fb69dbb9-91ee-4868-8c3c-93af3cd885be",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "---RETRIEVE---\n",
+ "\"Node 'retrieve':\"\n",
+ "'\\n---\\n'\n",
+ "---CHECK DOCUMENT RELEVANCE TO QUESTION---\n",
+ "---GRADE: DOCUMENT NOT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---GRADE: DOCUMENT NOT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---ASSESS GRADED DOCUMENTS---\n",
+ "---DECISION: GENERATE---\n",
+ "\"Node 'grade_documents':\"\n",
+ "'\\n---\\n'\n",
+ "---GENERATE---\n",
+ "---CHECK HALLUCINATIONS---\n",
+ "---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\n",
+ "---GRADE GENERATION vs QUESTION---\n",
+ "---DECISION: GENERATION ADDRESSES QUESTION---\n",
+ "\"Node 'generate':\"\n",
+ "'\\n---\\n'\n",
+ "('Short-term memory is used for in-context learning in agents, allowing them '\n",
+ " 'to learn quickly. Long-term memory enables agents to retain and recall vast '\n",
+ " 'amounts of information over extended periods. Agents can also utilize '\n",
+ " 'external tools like APIs to access additional information beyond what is '\n",
+ " 'stored in their memory.')\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pprint import pprint\n",
+ "\n",
+ "# Run\n",
+ "inputs = {\"question\": \"Explain how the different types of agent memory work?\"}\n",
+ "for output in app.stream(inputs):\n",
+ " for key, value in output.items():\n",
+ " # Node\n",
+ " pprint(f\"Node '{key}':\")\n",
+ " # Optional: print full state at each node\n",
+ " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
+ " pprint(\"\\n---\\n\")\n",
+ "\n",
+ "# Final generation\n",
+ "pprint(value[\"generation\"])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "4138bc51-8c84-4b8a-8d24-f7f470721f6f",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "---RETRIEVE---\n",
+ "\"Node 'retrieve':\"\n",
+ "'\\n---\\n'\n",
+ "---CHECK DOCUMENT RELEVANCE TO QUESTION---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---GRADE: DOCUMENT NOT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---ASSESS GRADED DOCUMENTS---\n",
+ "---DECISION: GENERATE---\n",
+ "\"Node 'grade_documents':\"\n",
+ "'\\n---\\n'\n",
+ "---GENERATE---\n",
+ "---CHECK HALLUCINATIONS---\n",
+ "---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\n",
+ "---GRADE GENERATION vs QUESTION---\n",
+ "---DECISION: GENERATION ADDRESSES QUESTION---\n",
+ "\"Node 'generate':\"\n",
+ "'\\n---\\n'\n",
+ "('Chain of thought prompting works by repeatedly prompting the model to ask '\n",
+ " 'follow-up questions to construct the thought process iteratively. This '\n",
+ " 'method can be combined with queries to search for relevant entities and '\n",
+ " 'content to add back into the context. It extends the thought process by '\n",
+ " 'exploring multiple reasoning possibilities at each step, creating a tree '\n",
+ " 'structure of thoughts.')\n"
+ ]
+ }
+ ],
+ "source": [
+ "inputs = {\"question\": \"Explain how chain of thought prompting works?\"}\n",
+ "for output in app.stream(inputs):\n",
+ " for key, value in output.items():\n",
+ " # Node\n",
+ " pprint(f\"Node '{key}':\")\n",
+ " # Optional: print full state at each node\n",
+ " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
+ " pprint(\"\\n---\\n\")\n",
+ "\n",
+ "# Final generation\n",
+ "pprint(value[\"generation\"])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "548f1c5b-4108-4aae-8abb-ec171b511b92",
+ "metadata": {},
+ "source": [
+ "LangSmith Traces - \n",
+ " \n",
+ "* https://smith.langchain.com/public/55d6180f-aab8-42bc-8799-dadce6247d9b/r\n",
+ "\n",
+ "* https://smith.langchain.com/public/1c6bf654-61b2-4fc5-9889-054b020c78aa/r"
+ ]
+ }
+ ],
+ "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.8"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/langgraph/source/examples/rag/langgraph_self_rag_local.ipynb b/langgraph/source/examples/rag/langgraph_self_rag_local.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..b7b729bdde52c8c302f56962115e5cb4dfb7a0da
--- /dev/null
+++ b/langgraph/source/examples/rag/langgraph_self_rag_local.ipynb
@@ -0,0 +1,755 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "345488d8",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
+ ]
+ },
+ {
+ "attachments": {
+ "5fca0a3e-d13d-4bfa-95ea-58203640cc7a.png": {
+ "image/png": "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AAEEEEAAAQQQQGDVCxA8WvWXAAAIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQEWA4FHFgjUEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAYNULEDxa9ZcAAAgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBARYDgUcWCNQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEBg1QsQPFr1lwAACCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBFgOBRxYI1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQGDVCxA8WvWXAAAIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQEWA4FHFgjUEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAYNULEDxa9ZcAAAgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBARYDgUcWCNQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEBg1QsQPFr1lwAACCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBFgOBRxYI1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQGDVCxA8WvWXAAAIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQEWA4FHFgjUEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAYNULEDxa9ZcAAAgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBARYDgUcWCNQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEBg1QsQPFr1lwAACCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBFgOBRxYI1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQGDVCxA8WvWXAAAIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQEWA4FHFgjUEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAYNULEDxa9ZcAAAgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBARYDgUcWCNQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEBg1QsQPFr1lwAACCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBFgOBRxYI1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQGDVCxA8WvWXAAAIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQEWA4FHFgjUEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAYNULEDxa9ZcAAAgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBARYDgUcWCNQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEBg1QsQPFr1lwAACCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBFgOBRxYI1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQGDVCxA8WvWXAAAIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQEWA4FHFgjUEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAYNULEDxa9ZcAAAgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBARYDgUcWCNQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEBg1QsQPFr1lwAACCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBFgOBRxYI1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQGDVCxA8WvWXAAAIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQEWA4FHFgjUEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAYNULEDxa9ZcAAAgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBARYDgUcWCNQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEBg1QsQPFr1lwAACCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBFgOBRxYI1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQGDVCxA8WvWXAAAIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQEWA4FHFgjUEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAYNULEDxa9ZcAAAgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBARYDgUcWCNQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEBg1QsQPFr1lwAACCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBFgOBRxYI1BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQGDVCxA8WvWXAAAIIIAA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BJSAElACSkAJKAEloASUgBJQAkpACRwdAi972csO2vDY2BiuuOKKWj3JJyfC0bnnnouPfOQj1noRjlpXeFjn2gnqjBJQAic1AQ10e1Jffj15JaAElIASUAJKQAkoASWgBJSAElACSkAJKAEloAQORiCVSuHCCy+sq3bLLbfg4osvxu9+9zvr5ff71XVUR0gXlIASWMkEVDxayVdP+64ElIASUAJKQAkoASWgBJSAElACSkAJKAEloASUwFElIOHsNm3aVHcMyXH0nOc8x1p3xx13WFNxHa1bt66uni4oASWgBFYqARWPVuqV034rASWgBJSAElACSkAJKAEloASUgBJQAkpACSgBJXDUCZx66ql1x/jYxz6GF73oRda6UqmEn/70p9a8WVdXWReUgBJQAiuUgIpHK/TCabeVgBJQAkpACSgBJaAElIASUAJKQAkoASWgBJSAEji6BJ7xjGdABCJT3ve+9+GGG24wi/jRj36E0dFRKxdSY1i7WiWdUQJKQAmsQAIqHq3Ai6ZdVgJKQAkoASWgBJSAElACSkAJKAEloASUgBJQAkrg6BJ47nOfi/Hx8dpB3vGOd+Cmm26qLcvMD3/4Q2v5hS98Yd16XVACSkAJrHQCKh6t9Cuo/VcCSkAJKAEloASUgBJQAkpACSgBJaAElIASUAJKYFkJXH/99di+fXutTRGN3v72t9eWZWbPnj24++67rTxHKh7VodEFJaAEngYEVDx6GlxEPQUloASUgBJQAkpACSgBJaAElIASUAJKQAkoASWgBJaHwBve8Abcf//9tcb+7M/+DBKurrEY15HkOvJ4PI2bdVkJKAElsKIJqHi0oi+fdl4JKAEloASUgBJQAkpACSgBJaAElIASUAJKQAkogeUi8N73vhc/+clPas29+MUvxkc+8pHasn1G8h1Fo1Fcd9119tU6rwSUgBJ4WhBQ8ehpcRn1JJSAElACSkAJKAEloASUgBJQAkpACSgBJaAElIASOBICn/rUp/CNb3yj1oTkPLr55ptry/aZu+66ywpr97KXvQy9vb32TTqvBJSAEnhaEFDx6GlxGfUklIASUAJKQAkoASWgBJSAElACSkAJKAEloASUgBI4XAK33norPve5z9V2v+SSSyDrFip/+MMfrE3qOlqIkK5XAkpgpRNwVFlW+klo/5WAElACSkAJKAEloASUgBJQAkpACSgBJaAElIASUAKHQ+Db3/423vWud9V2Pe+88/CDH/ygtrzQzH333YdnPetZC23W9UpACSiBFU1AxaMVffm080pACSgBJaAElIASUAJKQAkoASWgBJSAElACSkAJHC6Bn/70p3j9619f2/3UU0/Fz3/+89qyzigBJaAETlYCKh6drFdez1sJKAEloASUgBJQAkpACSgBJaAElIASUAJKQAmcxATuvfdevPKVr6wRWLVqFWSdFiWgBJSAEgBUPNK7QAkoASWgBJSAElACSkAJKAEloASUgBJQAkpACSiBk4rAli1bcO2119bOubm5GY888khtWWeUgBJQAic7ARWPTvY7QM9fCSgBJaAElIASUAJKQAkoASWgBJSAElACSkAJnEQE9u7di0svvbR2xh6PB08++WRtWWeUgBJQAkpAnUd6DygBJaAElIASUAJKQAkoASWgBJSAElACSkAJKAElcJIQSCQSOOuss+rOds+ePXXLuqAElIASUAIqHuk9oASUgBJQAkpACSgBJaAElIASUAJKQAkoASWgBJTASUKgr6+v7ky3bt2KUChUt04XlIASUAJKAHAqBCWgBJSAElACSkAJKAEloASUgBJQAkpACSgBJaAElMDTncD69evrTvGBBx5Q4aiOiC4oASWgBPYTUPFoPwudUwJKQAkoASWgBJSAElACSkAJKAEloASUgBJQAkrgaUhg06ZNKJVKtTO755570NHRUVvWGSWgBJSAEqgnoOJRPQ9dUgJKQAkoASWgBJSAElACSkAJKAEloASUgBJQAkrgaUTg/PPPRyqVqp3RnXfeibVr19aWdUYJKAEloAQOJKDi0YFMdI0SUAJKQAkoASWgBJSAElACSkAJKAEloASUgBJQAk8DApdccgkmJydrZ3L77bdDXEhalIASUAJKYHECKh4tzke3KgEloASUgBJQAkpACSgBJaAElIASUAJKQAkoASWwAglcffXVGBwcrPX8tttuw0UXXVRb1hkloASUgBJYmICKRwuz0S1KQAkoASWgBJSAElACSkAJKAEloASUgBJQAkpACaxAAs9//vPxxBNP1Hr++c9/HldeeWVtWWeUgBJQAkpgcQIqHi3OR7cqASWgBJSAElACSkAJKAEloASUgBJQAkpACSgBJbCCCFx33XV49NFHaz3+5Cc/CRGTtCgBJaAElMDSCah4tHRWWlMJKAEloASUgBJQAkpACSgBJaAElIASUAJKQAkogROYwJve9CY88MADtR6+//3vx/XXX19b1hkloASUgBJYGgEVj5bGSWspASWgBJSAElACSkAJKAEloASUgBJQAkpACSgBJXACE/j4xz+OO+64o9bDd77znXjta19bW9YZJaAElIASWDoBFY+WzkprKgEloASUgBJQAkpACSgBJaAElIASUAJKQAkoASVwAhL45je/iS9+8Yu1nr3xjW/EW97yltqyzigBJaAElMChEXBUWQ5tF62tBJSAElACSkAJKAEloASUgBJQAkpACSgBJaAElMDJTGBwcBD333+/heCZz3wment7jxuOX//613jVq15VO/5rXvMafOhDH6ot64wSUAJKQAkcOgEVjw6dme6hBJSAElACSkAJKAEloASUgBJQAkpACSgBJaAETkoCiUQCN998M77yla/Unf873vEOK0RcNBqtW3+0FwYGBnDZZZfVDnPdddfh05/+dG1ZZ5SAElACSuDwCKh4dHjcdC8loASUgBJQAkpACSgBJaAElIASUAJKQAkoASVwUhHYunUr/uqv/griOpqviPvo1ltvxRlnnDHf5qOyrq+vr9buNddcgy996Uu1ZZ1RAkpACSiBwyeg4tHhs9M9lYASUAJKQAkoASWgBJSAElACSkAJKAEloASUwElB4Dvf+Q4++MEPQpxHkVAI7/3Lv8RLrrwCiXQa//mLe/DPt9+OJOfFefTtb3/7mAhIGzZsQKFQsPhffvnl+PrXv35SXAs9SSWgBJTAsSCg4tGxoKzHUAJKQAkoASWgBJSAElACSkAJKAEloASUgBJQAiuUgAhH/z97ZwIXVfXF8eOCAgLijiKIuAHu+5ZrbqmVWm6ZppVlLpVlpqWZtpilZblU2qqmqaltbv9yyX3fxV1xQVFEkUVUUP/3XL2P94YBBpiBmeF3Pp/h3XffXb9vGOX95pwzYsQIufoGVUNoxttvk5cQkPR2JCyM3p42nY6JY3YISHXq1KGoqCi5hLp169LSpUv1y0EZBEAABEAgiwQgHmURILqDAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAgLMS4PxGX3zxhdxev06d6N3nB6S6VfZCGjJpEu04HCoFpM2bN8tjqh0yeaFFixYUJkQqtuDgYFq1apUs4wcIgAAIgID1CEA8sh5L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QDyy9zuE9YEACIAACIAACIAACIAACIAACIBAFghcuHCBRowYQT179qSpU6dSTExMFkbLetdq1arRrVu3ZC6Q6dOnU6NGjbI+KEYAARAAARsT8Pb2plOnTlH+/PmpVatWtGfPHrpx4wb16tWLVq5caZi9QoUKxAKSsu+++46SkpLUKY4gAAIg4BAEIB45xG3CIkEABEAABEAABEAABEAABEAABEAg4wRWr15Njz32mEz4vm3bNvriiy+oQ7t2dHDL5owPZoUederUodjYWHJzc5MeR4888ogVRsUQIAACIJA9BFg4YgGJhST+PPv999/lxIMGDaJffvnFsAgWjwICAmQdey2xgAQDARAAAUciAPHIke4W1goCIAACIAACIAACIAACIAACIAACFhJYvHgxvfTSS9LTyNPDg7q2ailf/E35zr2foSnvj7NwJOs0a9iwIUVFRclv7bPHUcuWLa0zMEYBARAAgWwmwGIQh7Lr0qWL9Ojk6d955x2aNm2athIOy6n3PuLcR+fOndOuowACIAAC9k4gz31h9r5IrA8EQAAEQAAEQAAEQAAEQAAEQAAEQMByAhymjsUjtjZNmtDEQS+TV6FC8vxIWBgN/mQSXYyMpKeEV9Ln33wj6235gz2Mzp8/T3ny5KFvxHwdOnSw5XQYGwRAAASyhQB/lh05coSGDx8uPTt5UvZCGj16tDZ/165dZYg7ruB8SR988IF2DQUQAAEQsGcCEI/s+e5gbSAAAiAAAiAAAiAAAiAAAiAAAiCQQQJ64Who71407OmnU4wQEx9Pz743jo4JIamRCL00++efteTuKRpnsYJzg5w+fVqOMmPGDOrcuXMWR0R3EAABEMgeAuXKlaNixYrJ8J/tRMjPunXrynxt+tl79OhB27dvp8OHD1PTpk0pOjraIBL9+eefNGzYMNmFw96tWrWKKlWqpB8CZRAAARCwSwL53hdmlyvDokAABEAABEAABEAABEAABEAABEAABDJEQC8cTRw6lPp36mi2f8ECBajTI01pw959tF98a3792rX0xJNPUsGCBc22z2xl+/bt6cSJE7L7l19+SU888URmh0I/EAABEMgRAix+b9y4kZYtW0YzZ86kXbt2SU/K27dvy5xG3bt3p3379tHIkSNp1qxZUkRaKz5TOUQdeyZVqVJF1vE49+7dI3d3d2rWrFmO7AWTggAI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Q3vEZpMACZAACZAACZAACQQrAat4xJxHwfotYL9JIOsEKB5lnRnPIAESIAESIAESIAESCAICc+bMkaZNm8rgwYPl0UcfVT1+4IEH5McffwyC3rOLJEACJEACJEACJEACgUiA4lEg3lX2iQT8Q4DikX+4slYSIAESIAESIAESIIEAIDBjxgxp1aqVfPzxxzJkyBDVo//7v/+T77//PgB6xy6QAAmQAAmQAAmQAAkEAwF6HgXDXWYfScD3BCge+Z4payQBEghAArt37w7AXrFLJEACJEAC3hCYNGmSdOjQQUaNGiUvvviiOmXQoEEyffr0TE9/7733JCEhIdNyLEACJEACJEACJEACJEACuUGAnke5QZnXIIHAIEDxKDDuI3tBAiTgJwKxsbHSpk0bNWjop0uwWhIgARIgAQcQGDdunHTp0kVee+01efPNN1WLn3zySZk8eXK6rV++fLm8//778vnnn6dbhgdIgARIgARIgARIgARIIDcJUDzKTdq8Fgk4mwDFI2ffP7aeBEjAzwRef/11gYBEIwESIAESIAGIQD179pTnnntO4FEEe+aZZ2TixIke4SDcXYMGDeSrr76SHTt2eCzDnSRAAiRAAiRAAiRAAiTgbwIMW+dvwqyfBAKTAMWjwLyv7BUJkIAPCCQlJcmGDRt8UBOrIAESIAESCBQCH374ofTu3VvgdfTpp5+qbg0dOlTGjx/vsYt33HGH4O/J2LFjPR7nThIgARIgARIgARIgARLITQL0PMpN2rwWCTibAMUjZ98/tp4ESMCPBEaPHu3idbRs2TI/Xo1VkwAJkAAJOIXA22+/Lf369ZOBAweaAhJyIY0ZMyZNFyAewfsI3klbt25Nc5w7SIAESIAESIAESIAESMDfBOh55G/CrJ8EApMAxaPAvK/sFQmQQA4JXLx4UX744QeXWubOneuyzQ0SIAESIIHgJYCwpvfff7+LgPTKK694zG/Ut29fwd+VadOmBS8w9pwESIAESIAESIAESMAWBOh5ZIvbwEaQgCMIUDxyxG1iI0mABHKbwOzZs2Xnzp3StGlT89ILFiww17lCAiRAAiRAAvA2euSRR5SApGm88cYb8sknn+hNtbzlllskJiZGpkyZInFxcS7HuEECJEACJEACJEACJEAC/iZAzyN/E2b9JBCYBCgeBeZ9Za9IgARySEB7HTVv3tysKTY2VubPn29uc4UESIAESIAEnnnmGRk8eLALiJEjRwpyI2krXLiwQEA6deqUTJ06Ve/mkgRIgARIgARIgARIgARyhQDFo1zBzIuQQMARoHgUcLeUHSIBEsgpgXXr1imRqGjRoi6eR6j3559/zmn1PJ8ESIAESCDACDz22GPy/PPPu/TqnXfeEXy0QTwKDw9XuY8OHz6sd3NJAiRAAiRAAiRAAiRAArlKICSEw8G5CpwXIwEHE+CvhYNvHptOAiTgHwJLlixRFXfu3FlKlChhXgQh7H755RdzmyskQAIkQAIkoAk8+OCD8tprr+lNtYT3EbyQYNHR0dKrVy+Jj49XApLayX9IgARIgARIgARIgARIIBcIWD2PcuFyvAQJkECAEKB4FCA3kt0gARLwHYEVK1aoyjp16uRS6c033yzHjx+XuXPnuuznBgmQAAmQAAmAwN133y1vv/22CwzkP0IeJNitt94qmOk5ceJEofeRCyZukAAJkAAJkAAJkAAJ5BKBK664IpeuxMuQAAk4nQDFI6ffQbafBEjApwQOHjwoy5Ytk7CwMGnXrp2Ehoaa9Xfv3l1Kly6tEp6bO7lCAiRAAiRAAhYCvXv3lo8++siyR+Tzzz+XV155RRo0aCD9+vWj95ELHW6QAAmQAAmQAAmQAAn4m4DV84jikb9ps34SCBwCFI8C516yJyRAAj4gsHz5cklJSZG2bdtKZGSkFClSxKwVIewgIC1YsEDWr19v7ucKCZAACZBA8BDA34nY2NgMO9yjRw8ZPXq0S5kxY8bIiy++qMQj/G2h95ELHm6QAAmQAAmQAAmQAAn4kQDFIz/CZdUkEMAEKB4F8M1l10iABLJOAIOCMHgdwQoXLqyW+p8bb7xRrU6fPl3v4pIESIAESCCICPTt21fatGkjgwcPlq1bt6bb8+uuu04mTJjgcnz8+PECEYneRy5YuEECJEACJEACJEACJJCLBBBGmUYCJEAC3hDgr4U3lFiGBEggaAggZF2+fPnSFY+aNWsmHTp0kGnTpsmBAweChgs7SgIkQAIkkEpgxowZcs0116i/A126dJE+ffrInDlzPOLBRIRZs2a5HIPHETyXSpYsSe8jFzLcIAESIAESIAESIAES8BcBeh75iyzrJYHAJkDxKLDvL3tHAiSQBQIrVqxQA3oIWVehQgV1prvnEXbC+ygxMTHdwcIsXJJFSYAESIAEHEagadOm8uWXXyqvIohI+NsxcOBA5Y303nvvybZt21x61LhxY1myZInLPohNZcuWZe4jFyrcIAESIAESIAESIAES8BcBa54j67q/rsd6SYAEAoMAxaPAuI/sBQmQgA8IrFq1StWiQ9ZhIzQ0NE3NyGVRrlw5mTdvXppj3EECJEACJBAcBPC3AiLSpEmT5LbbbpODBw/K+++/L9dee6088cQT8vvvv5sgKlasKFu2bDG3sYLt8PBwJUJllkPJ5URukAAJkAAJkAAJkAAJkEAWCdDzKIvAWJwESEARoHjELwIJkAAJ/EsAs8dhVvEIIewKFCjwb4nURaFChaR79+6yevVqWbBggcsxbpAACZAACQQXgVatWsmoUaPkp59+koceekhKlSolCG139913yx133CFTp06VlJQUKVKkiOzdu1ciIyNNQAkJCXL06FEZO3asuY8rJEACJEACJEACJEACJEACJEACJGAHAhSP7HAX2AYSIIE8J3Do0CEVegiDgDVq1HBpD8Qid7vzzjvVLuSuoJEACZAACZBArVq15LnnnpOff/5ZXn75ZWnSpIksXbpUhgwZIl27dpVPPvlE5cpbu3atoKzVvvjiC9m4caN1F9dJgARIgARIgARIgARIwGcE6HnkM5SsiASCigDFo6C63ewsCZBAegQQsu7ixYuCfEfu5kk8qlKligpT9Msvv8i6devcT+E2CZAACZBAkBKAZ1H//v1l5syZMmbMGOnZs6fs3LlTRo4cKddff728+uqrgtxILVu2NAnhZR5eSzQSIAESIAESIAESIAES8AcBq3gUEsLhYH8wZp0kEIgE+GsRiHeVfSIBEsgyAU8h63QlnsQjHLvhhhtUESQ+p5EACZAACZCAO4HOnTvLhx9+qLyRBg0aJBERESpPUrdu3SQqKkqqVq1qnoK8R8idRCMBEiABEiABEiABEiABfxK44oor/Fk96yYBEgggAhSPAuhmsiskQALZJ7BkyRIVYqhBgwZpKgkNDU2zDzs6duwojRo1EohHZ86c8ViGO0mABEiABEgA4VARvg4h7eCB1KZNG+WZtGvXLgkPDzcBwQu2b9++5jZXSIAESIAESIAESIAESMAXBKyeRxSPfEGUdZBAcBCgeBQc95m9JAESyIAAQs8hiXn79u09lkrP8wiF4X2E2eJz5871eC53kgAJkAAJkIAmgL8nffr0EeTL++abb6R3795y7tw5fVgtly9fLtOnT3fZxw0SIAESIAESIAESIAES8BUBike+Isl6SCDwCVA8Cvx7zB6SAAlkQgAzwWEdOnRQS/d/MhKPkL8if/78aja5+3ncJgESIAESIIH0CFx99dXy9ttvy7x58+SJJ56QAgUKqKIxMTHSq1ev9E7jfhIgARIgARIgARIgARLIMgF6HmUZGU8gARIwCFA84teABEgg6AnA86hJkybq4wlGRuJRpUqV5Nprr5X58+dLSkqKp9O5jwRIgARIgATSJYC8R08++aRs2rRJ6tWrJ88991y6ZXmABEiABEiABEiABEiABLJDwCoeZed8nkMCJBCcBCgeBed9Z69JgAT+JbBixQo5duxYuiHrUCwzl+7rrrtOCUerV68mVxIgARIgARLIFgFMVPjxxx+lS5cu2TqfJ5EACZAACZAACZAACZCANwRCQjgc7A0nliEBEhDJTwgkQAIkEMwESpYsKa1atUo3ZJ03bBBeKCoqStXjTXmWIQESIAESIAESIAESIAESIAESIAESIIHcImD1PMpsgmxutYnXIQESsD8Bikf2v0dsIQmQgB8J1KxZUyZNmpTjK0CAopEACZAACZAACZAACZAACZAACZAACZCAnQlQPLLz3WHbSMBeBOinaK/7wdaQAAnYkAAfrGx4U9gkEiABEiABEiABEiABEiABEiABEiABrwjQ88grTCxEAiTgRoDikRsQbpIACZBAegQuXryY3iHuJwESIAESIAESIAESIAESIAESIAESIAFbEqB4ZMvbwkaRgO0JUDyy/S1iA0mABOxCICkpyS5NYTtIgARIgARIgARIgARIgARIgARIgARIwCsC1ogqISEcDvYKGguRAAkIcx7xS0ACJEACXhI4ffq0FC9e3MvSLEYCJEACJEACeU8gJSVFRo8erRqCgYIBAwZI/vyXXwG2bt0qCxculIiICLn99tvzvsFsAQmQAAmQAAmQAAmQgM8JWD2PfF45KyQBEghYApffHAO2i+wYCZAACfiGQGJiom8qYi0kQAIkQAIkkEsEEHJ1xIgR5tWqV68unTt3Nre3bNmijhctWpTikUmFKyRAAiRAAiRAAiQQuASsXkiB20v2jARIwBcE6KfoC4qsgwRIICgIwPOIRgIkQAIkQAJOJjB+/HgnN59tJwESIAES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"
+ }
+ },
+ "cell_type": "markdown",
+ "id": "848ba742-7443-4123-8115-061da9823309",
+ "metadata": {},
+ "source": [
+ "# Self-RAG using local LLMs\n",
+ "\n",
+ "Self-RAG is a strategy for RAG that incorporates self-reflection / self-grading on retrieved documents and generations. \n",
+ "\n",
+ "In the [paper](https://arxiv.org/abs/2310.11511), a few decisions are made:\n",
+ "\n",
+ "1. Should I retrieve from retriever, `R` -\n",
+ "\n",
+ "* Input: `x (question)` OR `x (question)`, `y (generation)`\n",
+ "* Decides when to retrieve `D` chunks with `R`\n",
+ "* Output: `yes, no, continue`\n",
+ "\n",
+ "2. Are the retrieved passages `D` relevant to the question `x` -\n",
+ "\n",
+ "* * Input: (`x (question)`, `d (chunk)`) for `d` in `D`\n",
+ "* `d` provides useful information to solve `x`\n",
+ "* Output: `relevant, irrelevant`\n",
+ "\n",
+ "3. Are the LLM generation from each chunk in `D` is relevant to the chunk (hallucinations, etc) -\n",
+ "\n",
+ "* Input: `x (question)`, `d (chunk)`, `y (generation)` for `d` in `D`\n",
+ "* All of the verification-worthy statements in `y (generation)` are supported by `d`\n",
+ "* Output: `{fully supported, partially supported, no support`\n",
+ "\n",
+ "4. The LLM generation from each chunk in `D` is a useful response to `x (question)` -\n",
+ "\n",
+ "* Input: `x (question)`, `y (generation)` for `d` in `D`\n",
+ "* `y (generation)` is a useful response to `x (question)`.\n",
+ "* Output: `{5, 4, 3, 2, 1}`\n",
+ "\n",
+ "We will implement some of these ideas from scratch using [LangGraph](https://langchain-ai.github.io/langgraph/).\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9ed0a85a-a33b-40a6-99fa-2444bf57a6cc",
+ "metadata": {},
+ "source": [
+ "## Setup\n",
+ "\n",
+ "First let's install our required packages and set our API keys"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d7f9cc6d-a70c-433a-b0ad-ea47c5a0717e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%capture --no-stderr\n",
+ "%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph nomic[local]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "71c540ca",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import getpass\n",
+ "import os\n",
+ "\n",
+ "\n",
+ "def _set_env(key: str):\n",
+ " if key not in os.environ:\n",
+ " os.environ[key] = getpass.getpass(f\"{key}:\")\n",
+ "\n",
+ "\n",
+ "_set_env(\"NOMIC_API_KEY\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "05e8cf60",
+ "metadata": {},
+ "source": [
+ "
\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": "ccc6b6bd-a2fa-4a43-83b7-704b8b6fb855",
+ "metadata": {},
+ "source": [
+ "### LLMs\n",
+ "\n",
+ "#### Local Embeddings\n",
+ "\n",
+ "You can use `GPT4AllEmbeddings()` from Nomic, which can access use Nomic's recently released [v1](https://blog.nomic.ai/posts/nomic-embed-text-v1) and [v1.5](https://blog.nomic.ai/posts/nomic-embed-matryoshka) embeddings.\n",
+ "\n",
+ "\n",
+ "Follow the documentation [here](https://docs.gpt4all.io/gpt4all_python_embedding.html#supported-embedding-models).\n",
+ "\n",
+ "#### Local LLM\n",
+ "\n",
+ "(1) Download [Ollama app](https://ollama.ai/).\n",
+ "\n",
+ "(2) Download a `Mistral` model from various Mistral versions [here](https://ollama.ai/library/mistral) and Mixtral versions [here](https://ollama.ai/library/mixtral) available.\n",
+ "```\n",
+ "ollama pull mistral\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "bedffc73-6b10-42c8-8768-2085c8ed3398",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Ollama model name\n",
+ "local_llm = \"mistral\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ba68a46d-b617-4fdc-9113-fabdcf736feb",
+ "metadata": {},
+ "source": [
+ "## Create Index\n",
+ "\n",
+ "Let's index 3 blog posts."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c3bb9060-ad74-4470-9991-2ba167b6b8d8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
+ "from langchain_community.document_loaders import WebBaseLoader\n",
+ "from langchain_community.vectorstores import Chroma\n",
+ "from langchain_nomic.embeddings import NomicEmbeddings\n",
+ "\n",
+ "urls = [\n",
+ " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
+ "]\n",
+ "\n",
+ "docs = [WebBaseLoader(url).load() for url in urls]\n",
+ "docs_list = [item for sublist in docs for item in sublist]\n",
+ "\n",
+ "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
+ " chunk_size=250, chunk_overlap=0\n",
+ ")\n",
+ "doc_splits = text_splitter.split_documents(docs_list)\n",
+ "\n",
+ "# Add to vectorDB\n",
+ "vectorstore = Chroma.from_documents(\n",
+ " documents=doc_splits,\n",
+ " collection_name=\"rag-chroma\",\n",
+ " embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n",
+ ")\n",
+ "retriever = vectorstore.as_retriever()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cc60ff95-7e12-4004-b18e-a067a2dcc201",
+ "metadata": {},
+ "source": [
+ "## LLMs"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "3aad0c60-3208-48fb-af82-0024630b4da1",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'score': 'yes'}\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Retrieval Grader\n",
+ "\n",
+ "from langchain.prompts import PromptTemplate\n",
+ "from langchain_community.chat_models import ChatOllama\n",
+ "from langchain_core.output_parsers import JsonOutputParser\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
+ "\n",
+ "prompt = PromptTemplate(\n",
+ " template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
+ " Here is the retrieved document: \\n\\n {document} \\n\\n\n",
+ " Here is the user question: {question} \\n\n",
+ " If the document contains keywords related to the user question, grade it as relevant. \\n\n",
+ " It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n",
+ " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n",
+ " Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\"\"\",\n",
+ " input_variables=[\"question\", \"document\"],\n",
+ ")\n",
+ "\n",
+ "retrieval_grader = prompt | llm | JsonOutputParser()\n",
+ "question = \"agent memory\"\n",
+ "docs = retriever.get_relevant_documents(question)\n",
+ "doc_txt = docs[1].page_content\n",
+ "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "e5e45953-248d-492f-af28-d5e80c664c95",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " In an LLM-powered autonomous agent system, the Large Language Model (LLM) functions as the agent's brain. The agent has key components including memory, planning, and reflection mechanisms. The memory component is a long-term memory module that records a comprehensive list of agents’ experience in natural language. It includes a memory stream, which is an external database for storing past experiences. The reflection mechanism synthesizes memories into higher-level inferences over time and guides the agent's future behavior.\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Generate\n",
+ "\n",
+ "from langchain import hub\n",
+ "from langchain_core.output_parsers import StrOutputParser\n",
+ "\n",
+ "# Prompt\n",
+ "prompt = hub.pull(\"rlm/rag-prompt\")\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOllama(model=local_llm, temperature=0)\n",
+ "\n",
+ "\n",
+ "# Post-processing\n",
+ "def format_docs(docs):\n",
+ " return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
+ "\n",
+ "\n",
+ "# Chain\n",
+ "rag_chain = prompt | llm | StrOutputParser()\n",
+ "\n",
+ "# Run\n",
+ "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
+ "print(generation)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "56297862-df87-42a7-ba9d-310926dfb328",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'score': 'yes'}"
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Hallucination Grader\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
+ "\n",
+ "# Prompt\n",
+ "prompt = PromptTemplate(\n",
+ " template=\"\"\"You are a grader assessing whether an answer is grounded in / supported by a set of facts. \\n \n",
+ " Here are the facts:\n",
+ " \\n ------- \\n\n",
+ " {documents} \n",
+ " \\n ------- \\n\n",
+ " Here is the answer: {generation}\n",
+ " Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. \\n\n",
+ " Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n",
+ " input_variables=[\"generation\", \"documents\"],\n",
+ ")\n",
+ "\n",
+ "hallucination_grader = prompt | llm | JsonOutputParser()\n",
+ "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "e1dd9174-2df6-45b1-8e69-13381f579c39",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'score': 'yes'}"
+ ]
+ },
+ "execution_count": 10,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Answer Grader\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
+ "\n",
+ "# Prompt\n",
+ "prompt = PromptTemplate(\n",
+ " template=\"\"\"You are a grader assessing whether an answer is useful to resolve a question. \\n \n",
+ " Here is the answer:\n",
+ " \\n ------- \\n\n",
+ " {generation} \n",
+ " \\n ------- \\n\n",
+ " Here is the question: {question}\n",
+ " Give a binary score 'yes' or 'no' to indicate whether the answer is useful to resolve a question. \\n\n",
+ " Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n",
+ " input_variables=[\"generation\", \"question\"],\n",
+ ")\n",
+ "\n",
+ "answer_grader = prompt | llm | JsonOutputParser()\n",
+ "answer_grader.invoke({\"question\": question, \"generation\": generation})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "5216d92b-1ca1-4bcf-a34f-c99ec2766b54",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "' What is agent memory and how can it be effectively utilized in vector database retrieval?'"
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Question Re-writer\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOllama(model=local_llm, temperature=0)\n",
+ "\n",
+ "# Prompt\n",
+ "re_write_prompt = PromptTemplate(\n",
+ " template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n",
+ " for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n",
+ " Here is the initial question: \\n\\n {question}. Improved question with no preamble: \\n \"\"\",\n",
+ " input_variables=[\"generation\", \"question\"],\n",
+ ")\n",
+ "\n",
+ "question_rewriter = re_write_prompt | llm | StrOutputParser()\n",
+ "question_rewriter.invoke({\"question\": question})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3d3339b9-5f30-4d54-bfc9-9091c6035955",
+ "metadata": {},
+ "source": [
+ "# Graph \n",
+ "\n",
+ "Capture the flow in as a graph.\n",
+ "\n",
+ "## Graph state"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "90fb1dc6-c482-483a-8441-39965c401beb",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from typing import List\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "\n",
+ "class GraphState(TypedDict):\n",
+ " \"\"\"\n",
+ " Represents the state of our graph.\n",
+ "\n",
+ " Attributes:\n",
+ " question: question\n",
+ " generation: LLM generation\n",
+ " documents: list of documents\n",
+ " \"\"\"\n",
+ "\n",
+ " question: str\n",
+ " generation: str\n",
+ " documents: List[str]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "5324ea49-5745-47b5-a0a5-bf58c8babe46",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "### Nodes\n",
+ "\n",
+ "\n",
+ "def retrieve(state):\n",
+ " \"\"\"\n",
+ " Retrieve documents\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, documents, that contains retrieved documents\n",
+ " \"\"\"\n",
+ " print(\"---RETRIEVE---\")\n",
+ " question = state[\"question\"]\n",
+ "\n",
+ " # Retrieval\n",
+ " documents = retriever.get_relevant_documents(question)\n",
+ " return {\"documents\": documents, \"question\": question}\n",
+ "\n",
+ "\n",
+ "def generate(state):\n",
+ " \"\"\"\n",
+ " Generate answer\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, generation, that contains LLM generation\n",
+ " \"\"\"\n",
+ " print(\"---GENERATE---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # RAG generation\n",
+ " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
+ " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
+ "\n",
+ "\n",
+ "def grade_documents(state):\n",
+ " \"\"\"\n",
+ " Determines whether the retrieved documents are relevant to the question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates documents key with only filtered relevant documents\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # Score each doc\n",
+ " filtered_docs = []\n",
+ " for d in documents:\n",
+ " score = retrieval_grader.invoke(\n",
+ " {\"question\": question, \"document\": d.page_content}\n",
+ " )\n",
+ " grade = score[\"score\"]\n",
+ " if grade == \"yes\":\n",
+ " print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
+ " filtered_docs.append(d)\n",
+ " else:\n",
+ " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n",
+ " continue\n",
+ " return {\"documents\": filtered_docs, \"question\": question}\n",
+ "\n",
+ "\n",
+ "def transform_query(state):\n",
+ " \"\"\"\n",
+ " Transform the query to produce a better question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates question key with a re-phrased question\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---TRANSFORM QUERY---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # Re-write question\n",
+ " better_question = question_rewriter.invoke({\"question\": question})\n",
+ " return {\"documents\": documents, \"question\": better_question}\n",
+ "\n",
+ "\n",
+ "### Edges\n",
+ "\n",
+ "\n",
+ "def decide_to_generate(state):\n",
+ " \"\"\"\n",
+ " Determines whether to generate an answer, or re-generate a question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Binary decision for next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---ASSESS GRADED DOCUMENTS---\")\n",
+ " state[\"question\"]\n",
+ " filtered_documents = state[\"documents\"]\n",
+ "\n",
+ " if not filtered_documents:\n",
+ " # All documents have been filtered check_relevance\n",
+ " # We will re-generate a new query\n",
+ " print(\n",
+ " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
+ " )\n",
+ " return \"transform_query\"\n",
+ " else:\n",
+ " # We have relevant documents, so generate answer\n",
+ " print(\"---DECISION: GENERATE---\")\n",
+ " return \"generate\"\n",
+ "\n",
+ "\n",
+ "def grade_generation_v_documents_and_question(state):\n",
+ " \"\"\"\n",
+ " Determines whether the generation is grounded in the document and answers question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Decision for next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---CHECK HALLUCINATIONS---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ " generation = state[\"generation\"]\n",
+ "\n",
+ " score = hallucination_grader.invoke(\n",
+ " {\"documents\": documents, \"generation\": generation}\n",
+ " )\n",
+ " grade = score[\"score\"]\n",
+ "\n",
+ " # Check hallucination\n",
+ " if grade == \"yes\":\n",
+ " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n",
+ " # Check question-answering\n",
+ " print(\"---GRADE GENERATION vs QUESTION---\")\n",
+ " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
+ " grade = score[\"score\"]\n",
+ " if grade == \"yes\":\n",
+ " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
+ " return \"useful\"\n",
+ " else:\n",
+ " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n",
+ " return \"not useful\"\n",
+ " else:\n",
+ " print(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n",
+ " return \"not supported\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bdf3826c-b668-4f0b-bf83-81c40baaaf02",
+ "metadata": {},
+ "source": [
+ "## Build Graph\n",
+ "\n",
+ "This just follows the flow we outlined in the figure above."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "5605dee4-b2df-46ae-a640-cc2ed90c21a6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langgraph.graph import END, StateGraph, START\n",
+ "\n",
+ "workflow = StateGraph(GraphState)\n",
+ "\n",
+ "# Define the nodes\n",
+ "workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
+ "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
+ "workflow.add_node(\"generate\", generate) # generate\n",
+ "workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
+ "\n",
+ "# Build graph\n",
+ "workflow.add_edge(START, \"retrieve\")\n",
+ "workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"grade_documents\",\n",
+ " decide_to_generate,\n",
+ " {\n",
+ " \"transform_query\": \"transform_query\",\n",
+ " \"generate\": \"generate\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"transform_query\", \"retrieve\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"generate\",\n",
+ " grade_generation_v_documents_and_question,\n",
+ " {\n",
+ " \"not supported\": \"generate\",\n",
+ " \"useful\": END,\n",
+ " \"not useful\": \"transform_query\",\n",
+ " },\n",
+ ")\n",
+ "\n",
+ "# Compile\n",
+ "app = workflow.compile()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "105ae1b5-6963-4186-bb83-6d6cb96d095f",
+ "metadata": {},
+ "source": [
+ "## Run\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "26a64f7d-0c14-4e31-a67f-63021dee626e",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "---RETRIEVE---\n",
+ "\"Node 'retrieve':\"\n",
+ "'\\n---\\n'\n",
+ "---CHECK DOCUMENT RELEVANCE TO QUESTION---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "\"Node 'grade_documents':\"\n",
+ "'\\n---\\n'\n",
+ "---ASSESS GRADED DOCUMENTS---\n",
+ "---DECISION: GENERATE---\n",
+ "---GENERATE---\n",
+ "\"Node 'generate':\"\n",
+ "'\\n---\\n'\n",
+ "---CHECK HALLUCINATIONS---\n",
+ "---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\n",
+ "---GRADE GENERATION vs QUESTION---\n",
+ "---DECISION: GENERATION ADDRESSES QUESTION---\n",
+ "\"Node '__end__':\"\n",
+ "'\\n---\\n'\n",
+ "(' In a LLM-powered autonomous agent system, memory is a key component that '\n",
+ " 'enables agents to store and retrieve information. There are different types '\n",
+ " 'of memory in human brains, such as sensory memory which retains impressions '\n",
+ " 'of sensory information for a few seconds, and long-term memory which records '\n",
+ " \"experiences for extended periods (Lil'Log, 2023). In the context of LLM \"\n",
+ " 'agents, memory is often implemented as an external database or memory stream '\n",
+ " \"(Lil'Log, 2023). The agent can consult this memory to inform its behavior \"\n",
+ " 'based on relevance, recency, and importance. Additionally, reflection '\n",
+ " 'mechanisms synthesize memories into higher-level inferences over time and '\n",
+ " \"guide the agent's future behavior (Lil'Log, 2023).\")\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pprint import pprint\n",
+ "\n",
+ "# Run\n",
+ "inputs = {\"question\": \"Explain how the different types of agent memory work?\"}\n",
+ "for output in app.stream(inputs):\n",
+ " for key, value in output.items():\n",
+ " # Node\n",
+ " pprint(f\"Node '{key}':\")\n",
+ " # Optional: print full state at each node\n",
+ " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
+ " pprint(\"\\n---\\n\")\n",
+ "\n",
+ "# Final generation\n",
+ "pprint(value[\"generation\"])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f9a27907-2611-4791-910b-0d66c59f5cf5",
+ "metadata": {},
+ "source": [
+ "Trace: \n",
+ "\n",
+ "https://smith.langchain.com/public/4163a342-5260-4852-8602-bda3f95177e7/r"
+ ]
+ }
+ ],
+ "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.8"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/langgraph/source/examples/rag/langgraph_self_rag_pinecone_movies.ipynb b/langgraph/source/examples/rag/langgraph_self_rag_pinecone_movies.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..cb6dd761f471a2dc2f77509e07a749863733cba5
--- /dev/null
+++ b/langgraph/source/examples/rag/langgraph_self_rag_pinecone_movies.ipynb
@@ -0,0 +1,758 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "403aeb6e",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. Please see the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview) for the most current information and resources."
+ ]
+ },
+ {
+ "attachments": {
+ "15cba0ab-a549-4909-8373-fb761e384eff.png": {
+ "image/png": "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AAIIIIAAAggggAACCCBA4MR3AAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIC4BAqe4+NgZAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEECAwInvAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQFwCBE5x8bEzAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAgRPfAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgbgECJzi4mNnBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABAie+AwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAnEJEDjFxcfOCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACBE58BxBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBOISIHCKi4+dEUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEECJz4DiCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCMQlQOAUFx87I4AAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIEDjxHUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEIhLgMApLj52RgABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQIHDiO4AAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBCXAIFTXHzsjAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggQODEdwABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQCAuAQKnuPjYGQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAgMCJ7wACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBcAgROcfGxMwIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAIET3wEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIG4BAic4uJjZwQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQInvgMIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAJxCRA4xcXHzggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgROfAcQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQTiEiBwiouPnRFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBAic+A4ggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgjEJUDgFBcfOyOAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCBA48R1AAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBCIS4DAKS4+dkYAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEECBw4juAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCAQlwCBU1x87IwAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIEDgxHcAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAgLgECp7j42BkBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQIDAie8AAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAXAIETnHxsTMCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggACBE98BBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBuAQInOLiY2cEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAECJ74DCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACcQkQOMXFx84IIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIETnwHEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEE4hIgcIqLj50RQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQInPgOIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIxCVA4BQXHzsjgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggQOPEdQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQiEuAwCkuPnZGAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAgcOI7gAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEJcAgVNcfOyMAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCBA4MR3AAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIC6BwnHtzc4IIIAAAggggAACCCCA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vRYuXGi7du2yihUrWq1atax27dqmqqb82gic8uud57oRQAABBBBAAAEEEEAAAQQQQAABBBBAIMcFVBHx+uuv27Rp09xn5ZexXHIclg9AAIGEEyBwSrhbwgkhgAACCCCAAAIIIIAAAggggAACCCCAQF4WWLFihQ0fPtzef/99mzNnTsSldO/e3e6///6IZcwggAACySCQfzoPTIa7xTUggAACCCCAAAIIIIAAAggggAACCCCAQMIKKGgaOnSoey1fvtydZ+XKlc2f1oIKFSok7PlzYggggEA8AgRO8eixLwIIIIAAAggggAACCCCAAAIIIIAAAgjke4HooKlAgQLWq1cvmzBhgv3www+BT48ePez6668P5plAAAEEkkmALvWS6W5yLQgggAACCCCAAAIIIIAAAggggAACCCCwTwWefPLJoKKpZMmSLmg699xz7aqrrrLJkycH59KwYUMbPXp0MM8EAgggkGwCBE7Jdke5HgQQQAABBBBAAAEEEEAAAQQQQAABBBDIUYGtW7fa4MGD7dVXX7Vly5ZZpUqVXNCk8ZnUunXrFhE2aZnGczryyCM1SUMAAQSSUoAu9ZLytnJRCCCAAAIIIIAAAggggAACCCCAAAIIIJATAm+//bYLm3799VcrX768PfbYY3bOOecEH3XdddelCptOPvlkwqZAiAkEEEhWAQKnZL2zXBcCCCCAAAIIIIAAAggggAACCCCAAAIIZJvAqFGjXNA0adIkK1y4sKto6t27d8TxH3roIfvwww8jlmnmrLPOSrWMBQgggECyCRA4Jdsd5XoQQAABBBBAAAEEEEAAAQQQQAABBBBAINsENA6Tus5T4KSm7vJuuukmq1y5csRnaJsXXnghWKZu9lasWGEtWrSwDh06BMuZQAABBJJVgMApWe8s14UAAggggAACCCCAAAIIIIAAAggggAACWRb46aefbMiQIaYu9NROOeUUu/jii12AFH3Q0aNH27333hssrlu3rv35559unuqmgIUJBBBIcgECpyS/wVweAggggAACCCCAAAIIIIAAAggggAACCGRcYOnSpfbSSy+5l/Y64YQT7Pzzz7f27dvHPMi0adOsR48ewbo6derYoYce6gKn+vXr29lnnx2sYwIBBBBIZgECp2S+u1wbAggggAACCCCAAAIIIIAAAggggAACCGRK4KKLLrLZs2dbq1atXEXTSSedlOb+M2fOjAiU1I1ev3797LLLLnP7KGwqVKhQmvuzAgEEEEgmAQKnZLqbXAsCCCCAAAIIIIAAAggggAACCCCAAAIIxCVw5ZVX2q5du+zcc89N9zhz5861k08+OdimVKlSNmjQIPvuu+9s06ZNbownqpsCHiYQQCAfCBA45YObzCUigAACCCCAAAIIIIAAAggggAACCCCAQMYEunbtutcNly9fbm3btg22K1iwoAubDj/8cHvwwQfdcoVNFSpUCLZhAgEEEEh2gYLJfoFcHwIIIIAAAggggAACCCCAAAIIIIAAAgggkF0CGzdutObNm0cc7sUXX7Sjjz7apk6d6l7FixeP6GovYmNmEEAAgSQVIHBK0hvLZSGAAAIIIIAAAggggAACCCCAAAIIIIBA9gqoq70mTZpEHPTpp5+2Dh06uGUjR45076puqlu3bsR2zCCAAALJLkDglOx3mOtDAAEEEEAAAQQQQAABBBBAAAEEEEAAgWwRqF+/fsRx+vXrZ6effrpbtnPnThs1apSb9pdFbMwMAgggkOQCBE5JfoO5PAQQQAABBBBAAAEEEEAAAQQQQAABBBCIX+Coo44yhUp+u/POO+3888/3Z+1///ufLVu2zI3tFN3lXrAREwgggEASCxA4JfHN5dIQQAABBBBAAAEEEEAAAQQQQAABBBBAIH6BE0880VasWBEc6IYbbrAePXoE85r46KOP3Pxpp50WsZwZBBBAIL8IEDjllzvNdSKAAAIIIIAAAggggAACCCCAAAIIIIBApgW6detms2bNCvZT0HT99dcH85qYP3++ffHFF27cJgKnCBpmEEAgHwkQOOWjm82lIoAAAggggAACCCCAAAIIIIAAAggggEDGBa666iqbPHlysMOFF15o6kovuvnVTRq7qUiRItGrmUcAAQTyhQCBU764zVwkAggggAACCCCAAAIIIIAAAggggAACCGRG4I477rBPP/002OXMM8+0Bx98MJgPT2j8ppSUFDv77LPDi5lGAAEE8pUAgVO+ut1cLAIIIIAAAggggAACCCCAAAIIIIAAAgjsTaB///725ptvBptpDKcnn3wymA9PjB071nW517VrV6tevXp4FdMIIIBAvhIgcMpXt5uLRQABBBBAAAEEEEAAAQQQQAABBBBAAIH0BAYNGmQDBgwINjnmmGNMy9JqP/74o1tF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WRse/evbrdvQHW85l3qeValIgkbFJZ7PNHd6eux+ldn/PZo65saw6uHZrf1zGUSF48Xt+X7+wNTwi69PDjFAlVVG1lWFZ5fMah2jOJNpYFXfMP8TDcn7U8KggV1vQhEzqH72tabw1E8+FZrGDc5nNWX9bmw7cuJMh0cxaLd1T3N45qV3w4+Xvx4WPfpKOP3B+VS/h+66NtlkqUFVCBAgQIAAAQIECBC4bgX6XEzKdft1PowAAQIECBAgQIAAgbIWiNk53022n3t929HM75h6S2144ge3Zz7rrcaDxz8I7xw4FaqSANWtjTWhX9s2dlfye/F8pSPJOA3J1oEjkmBUruxNtrWLAanqJAg1oF9F6JMdv8p1z19PJIG37ftPhpOnLwXgYlCqceiAUF/TP3OM7W0ZTfkBim5+uXh6mDN5aFGrKgECBAgQIECAAAECBC4vIOB0eRtPCBAgQIAAAQIECBAoAYG45dzvX9gRlj+3o8NsbhpSFf6WZB8pVyaw9s3D4UePv9bhpeH1A8KyBz4WmkbVdHimgQABAgQIECBAgAABAp0JdL1fQ2dve0aAAAECBAgQIECAAIFeFogZP9/5wvhw98zGsGr93vDCvw+E/UmG0Kkkm6f5VNdb6vXy9Mpy+CPNl7bZi7Y11ZVhWnJe05c/OSLMaRrarXOkyvKjTZoAAQIECBAgQIAAgV4VkOHUq7wGJ0CAAAECBAgQIECgNwXiBuHd3XauN+dRjmOzK8dVM2cCBAgQIECAAAECpSsg4FS6a2NmBAgQIECAAAECBAgQIECAAAECBAgQIECAAIGyELihLGZpkgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAiUrIOBUsktjYgQIECBAgAABAgQIECBAgAABAgQIECBAgACB8hAQcCqPdTJLAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDJCgg4lezSmBgBAgQIECBAgAABAgQIECBAgAABAgQIECBAoDwEBJzKY53MkgABAgQIECBAgAABAgQIECBAgAABAgQIECBQsgICTiW7NCZGgAABAgQIECBAgAABAgQIECBAgAABAgQIECgPAQGn8lgnsyRAgAABAgQIECBAgAABAgQIECBAgAABAgQIlKyAgFPJLo2JESBAgAABAgQIECBAgAABAgQIECBAgAABAgTKQ0DAqTzWySwJECBAgAABAgQIECBAgAABAgQIECBAgAABAiUrIOBUsktjYgQIECBAgAABAgQIECBAgAABAgQIECBAgACB8hAQcCqPdTJLAgQIECBAgAABAgQIECBAgAABAgQIECBAgEDJCgg4lezSmBgBAgQIECBAgAABAgQIECBAgAABAgQIECBAoDwEBJzKY53MkgABAgQIECBAgAABAgQIECBAgAABAgQIECBQsgICTiW7NCZGgAABAgQIECBAgAABAgQIECBAgAABAgQIECgPAQGn8lgnsyRAgAABAgQIECBAgAABAgQIECBAgAABAgQIlKzA/wHZpDlUM4Q3oAAAAABJRU5ErkJggg=="
+ }
+ },
+ "cell_type": "markdown",
+ "id": "919fe33c-0149-4f7d-b200-544a18986c9a",
+ "metadata": {},
+ "source": [
+ "# Self RAG\n",
+ "\n",
+ "Self-RAG is a strategy for RAG that incorporates self-reflection / self-grading on retrieved documents and generations. \n",
+ "\n",
+ "[Paper](https://arxiv.org/abs/2310.11511)\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "72f3ee57-68ab-4040-bd36-4014e2a23d96",
+ "metadata": {},
+ "source": [
+ "# Environment "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a384cc48-0425-4e8f-aafc-cfb8e56025c9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%pip install -qU langchain-pinecone langchain-openai langchainhub langgraph"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "532d91fb-381e-4e11-b3b1-254321351773",
+ "metadata": {},
+ "source": [
+ "### Tracing\n",
+ "\n",
+ "Use [LangSmith](https://docs.smith.langchain.com/) for tracing (shown at bottom)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ccc3dae5-1df6-48ca-af8a-50f0e6128876",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "\n",
+ "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
+ "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n",
+ "os.environ[\"LANGCHAIN_API_KEY\"] = \"\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "88637820",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "\n",
+ "os.environ[\"LANGCHAIN_PROJECT\"] = \"pinecone-devconnect\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c27bebdc-be71-4130-ab9d-42f09f87658b",
+ "metadata": {},
+ "source": [
+ "## Retriever\n",
+ " \n",
+ "Let's use Pinecone's sample movies database"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "565a6d44-2c9f-4fff-b1ec-eea05df9350d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langchain_openai import OpenAIEmbeddings\n",
+ "from langchain_pinecone import PineconeVectorStore\n",
+ "\n",
+ "# use pinecone movies database\n",
+ "\n",
+ "# Add to vectorDB\n",
+ "vectorstore = PineconeVectorStore(\n",
+ " embedding=OpenAIEmbeddings(),\n",
+ " index_name=\"sample-movies\",\n",
+ " text_key=\"summary\",\n",
+ ")\n",
+ "retriever = vectorstore.as_retriever()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "1aeb4373",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "# Avatar\n",
+ "On the alien world of Pandora, paraplegic Marine Jake Sully uses an avatar to walk again and becomes torn between his mission and protecting the planet's indigenous Na'vi people. The film stars Sam Worthington, Zoe Saldana, and Sigourney Weaver.\n",
+ "\n",
+ "# Top Gun: Maverick\n",
+ "Capt. Pete \"Maverick\" Mitchell, after decades of service as one of the Navy's top aviators, confronts his past while training a new squad for a dangerous mission. Tom Cruise reprises his iconic role, showcasing thrilling aerial stunts.\n",
+ "\n",
+ "# Jurassic World Dominion\n",
+ "The film concludes the story of Jurassic World, with humanity now living alongside dinosaurs. It follows Owen Grady and Claire Dearing as they navigate this new world.\n",
+ "\n",
+ "# Aquaman\n",
+ "Arthur Curry learns he is the heir to the underwater kingdom of Atlantis and must step forward to lead his people and be a hero to the world. Stars Jason Momoa, Amber Heard, and Willem Dafoe.\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "docs = retriever.invoke(\"James Cameron\")\n",
+ "for doc in docs:\n",
+ " print(\"# \" + doc.metadata[\"title\"])\n",
+ " print(doc.page_content)\n",
+ " print()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "29c12f74-53e2-43cc-896f-875d1c5d9d93",
+ "metadata": {},
+ "source": [
+ "## Structured Output - Retrieval Grader"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "1fafad21-60cc-483e-92a3-6a7edb1838e3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "### Retrieval Grader\n",
+ "\n",
+ "from langchain import hub\n",
+ "from langchain_core.pydantic_v1 import BaseModel, Field\n",
+ "from langchain_openai import ChatOpenAI\n",
+ "\n",
+ "\n",
+ "# Data model\n",
+ "class GradeDocuments(BaseModel):\n",
+ " \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n",
+ "\n",
+ " binary_score: str = Field(\n",
+ " description=\"Documents are relevant to the question, 'yes' or 'no'\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# https://smith.langchain.com/hub/efriis/self-rag-retrieval-grader\n",
+ "grade_prompt = hub.pull(\"efriis/self-rag-retrieval-grader\")\n",
+ "\n",
+ "# LLM with function call\n",
+ "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
+ "structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
+ "\n",
+ "retrieval_grader = grade_prompt | structured_llm_grader"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "2e79eed8",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Arthur Curry learns he is the heir to the underwater kingdom of Atlantis and must step forward to lead his people and be a hero to the world. Stars Jason Momoa, Amber Heard, and Willem Dafoe.\n",
+ "binary_score='yes'\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Test the retrieval grader\n",
+ "question = \"movies starring jason momoa\"\n",
+ "docs = retriever.invoke(question)\n",
+ "doc_txt = docs[0].page_content\n",
+ "print(doc_txt)\n",
+ "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c06abc65",
+ "metadata": {},
+ "source": [
+ "# Generation Step\n",
+ "\n",
+ "Standard RAG"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "dcd77cc1-4587-40ec-b633-5364eab9e1ec",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Jason Momoa stars in the movies \"Aquaman\" and \"Furious 7.\"\n"
+ ]
+ }
+ ],
+ "source": [
+ "### Generate\n",
+ "\n",
+ "from langchain import hub\n",
+ "from langchain_core.output_parsers import StrOutputParser\n",
+ "\n",
+ "# Prompt\n",
+ "prompt = hub.pull(\"rlm/rag-prompt\")\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n",
+ "\n",
+ "# Chain\n",
+ "rag_chain = prompt | llm | StrOutputParser()\n",
+ "\n",
+ "# Run\n",
+ "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
+ "print(generation)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "e78931ec-940c-46ad-a0b2-f43f953f1fd7",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Jason Momoa stars in the movies \"Aquaman\" and \"Furious 7.\"\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "GradeHallucinations(binary_score='yes')"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Hallucination Grader\n",
+ "\n",
+ "\n",
+ "# Data model\n",
+ "class GradeHallucinations(BaseModel):\n",
+ " \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n",
+ "\n",
+ " binary_score: str = Field(\n",
+ " description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# LLM with function call\n",
+ "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
+ "structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n",
+ "\n",
+ "# https://smith.langchain.com/hub/efriis/self-rag-hallucination-grader\n",
+ "hallucination_prompt = hub.pull(\"efriis/self-rag-hallucination-grader\")\n",
+ "\n",
+ "hallucination_grader = hallucination_prompt | structured_llm_grader\n",
+ "print(generation)\n",
+ "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "bd62276f-bf26-40d0-8cff-e07b10e00321",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "movies starring jason momoa\n",
+ "Jason Momoa stars in the movies \"Aquaman\" and \"Furious 7.\"\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "GradeAnswer(binary_score='yes')"
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Answer Grader\n",
+ "\n",
+ "\n",
+ "# Data model\n",
+ "class GradeAnswer(BaseModel):\n",
+ " \"\"\"Binary score to assess answer addresses question.\"\"\"\n",
+ "\n",
+ " binary_score: str = Field(\n",
+ " description=\"Answer addresses the question, 'yes' or 'no'\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "# LLM with function call\n",
+ "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
+ "structured_llm_grader = llm.with_structured_output(GradeAnswer)\n",
+ "\n",
+ "# Prompt\n",
+ "answer_prompt = hub.pull(\"efriis/self-rag-answer-grader\")\n",
+ "\n",
+ "answer_grader = answer_prompt | structured_llm_grader\n",
+ "print(question)\n",
+ "print(generation)\n",
+ "answer_grader.invoke({\"question\": question, \"generation\": generation})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "c6f4c70e-1660-4149-82c0-837f19fc9fb5",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "movies starring jason momoa\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "'Which movies feature Jason Momoa in a leading role?'"
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "### Question Re-writer\n",
+ "\n",
+ "# LLM\n",
+ "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
+ "\n",
+ "# Prompt\n",
+ "re_write_prompt = hub.pull(\"efriis/self-rag-question-rewriter\")\n",
+ "\n",
+ "question_rewriter = re_write_prompt | llm | StrOutputParser()\n",
+ "print(question)\n",
+ "question_rewriter.invoke({\"question\": question})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "276001c5-c079-4e5b-9f42-81a06704d200",
+ "metadata": {},
+ "source": [
+ "# Graph \n",
+ "\n",
+ "Capture the flow in as a graph.\n",
+ "\n",
+ "## Graph state"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "f1617e9e-66a8-4c1a-a1fe-cc936284c085",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from typing import List\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "\n",
+ "class GraphState(TypedDict):\n",
+ " \"\"\"\n",
+ " Represents the state of our graph.\n",
+ "\n",
+ " Attributes:\n",
+ " question: question\n",
+ " generation: LLM generation\n",
+ " documents: list of documents\n",
+ " \"\"\"\n",
+ "\n",
+ " question: str\n",
+ " generation: str\n",
+ " documents: List[str]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "add509d8-6682-4127-8d95-13dd37d79702",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "### Nodes\n",
+ "\n",
+ "\n",
+ "def retrieve(state):\n",
+ " \"\"\"\n",
+ " Retrieve documents\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, documents, that contains retrieved documents\n",
+ " \"\"\"\n",
+ " print(\"---RETRIEVE---\")\n",
+ " question = state[\"question\"]\n",
+ "\n",
+ " # Retrieval\n",
+ " documents = retriever.invoke(question)\n",
+ " return {\"documents\": documents, \"question\": question}\n",
+ "\n",
+ "\n",
+ "def generate(state):\n",
+ " \"\"\"\n",
+ " Generate answer\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): New key added to state, generation, that contains LLM generation\n",
+ " \"\"\"\n",
+ " print(\"---GENERATE---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # RAG generation\n",
+ " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
+ " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
+ "\n",
+ "\n",
+ "def grade_documents(state):\n",
+ " \"\"\"\n",
+ " Determines whether the retrieved documents are relevant to the question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates documents key with only filtered relevant documents\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # Score each doc\n",
+ " filtered_docs = []\n",
+ " for d in documents:\n",
+ " score = retrieval_grader.invoke(\n",
+ " {\"question\": question, \"document\": d.page_content}\n",
+ " )\n",
+ " grade = score.binary_score\n",
+ " if grade == \"yes\":\n",
+ " print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
+ " filtered_docs.append(d)\n",
+ " else:\n",
+ " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n",
+ " continue\n",
+ " return {\"documents\": filtered_docs, \"question\": question}\n",
+ "\n",
+ "\n",
+ "def transform_query(state):\n",
+ " \"\"\"\n",
+ " Transform the query to produce a better question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " state (dict): Updates question key with a re-phrased question\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---TRANSFORM QUERY---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ "\n",
+ " # Re-write question\n",
+ " better_question = question_rewriter.invoke({\"question\": question})\n",
+ " return {\"documents\": documents, \"question\": better_question}"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "09fc91b4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "### Edges\n",
+ "\n",
+ "\n",
+ "def decide_to_generate(state):\n",
+ " \"\"\"\n",
+ " Determines whether to generate an answer, or re-generate a question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Binary decision for next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---ASSESS GRADED DOCUMENTS---\")\n",
+ " state[\"question\"]\n",
+ " filtered_documents = state[\"documents\"]\n",
+ "\n",
+ " if not filtered_documents:\n",
+ " # All documents have been filtered check_relevance\n",
+ " # We will re-generate a new query\n",
+ " print(\n",
+ " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
+ " )\n",
+ " return \"transform_query\"\n",
+ " else:\n",
+ " # We have relevant documents, so generate answer\n",
+ " print(\"---DECISION: GENERATE---\")\n",
+ " return \"generate\"\n",
+ "\n",
+ "\n",
+ "def grade_generation_v_documents_and_question(state):\n",
+ " \"\"\"\n",
+ " Determines whether the generation is grounded in the document and answers question.\n",
+ "\n",
+ " Args:\n",
+ " state (dict): The current graph state\n",
+ "\n",
+ " Returns:\n",
+ " str: Decision for next node to call\n",
+ " \"\"\"\n",
+ "\n",
+ " print(\"---CHECK HALLUCINATIONS---\")\n",
+ " question = state[\"question\"]\n",
+ " documents = state[\"documents\"]\n",
+ " generation = state[\"generation\"]\n",
+ "\n",
+ " score = hallucination_grader.invoke(\n",
+ " {\"documents\": documents, \"generation\": generation}\n",
+ " )\n",
+ " grade = score.binary_score\n",
+ "\n",
+ " # Check hallucination\n",
+ " if grade == \"yes\":\n",
+ " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n",
+ " # Check question-answering\n",
+ " print(\"---GRADE GENERATION vs QUESTION---\")\n",
+ " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
+ " grade = score.binary_score\n",
+ " if grade == \"yes\":\n",
+ " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
+ " return \"useful\"\n",
+ " else:\n",
+ " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n",
+ " return \"not useful\"\n",
+ " else:\n",
+ " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n",
+ " return \"not supported\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "61cd5797-1782-4d78-a277-8196d13f3e1b",
+ "metadata": {},
+ "source": [
+ "## Build Graph\n",
+ "\n",
+ "The just follows the flow we outlined in the figure above."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "0e09ca9f-e36d-4ef4-a0d5-79fdbada9fe0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langgraph.graph import END, StateGraph, START\n",
+ "\n",
+ "workflow = StateGraph(GraphState)\n",
+ "\n",
+ "# Define the nodes\n",
+ "workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
+ "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
+ "workflow.add_node(\"generate\", generate) # generate\n",
+ "workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
+ "\n",
+ "# Build graph\n",
+ "workflow.add_edge(START, \"retrieve\")\n",
+ "workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"grade_documents\",\n",
+ " decide_to_generate,\n",
+ " {\n",
+ " \"transform_query\": \"transform_query\",\n",
+ " \"generate\": \"generate\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"transform_query\", \"retrieve\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"generate\",\n",
+ " grade_generation_v_documents_and_question,\n",
+ " {\n",
+ " \"not supported\": \"generate\",\n",
+ " \"useful\": END,\n",
+ " \"not useful\": \"transform_query\",\n",
+ " },\n",
+ ")\n",
+ "\n",
+ "# Compile\n",
+ "app = workflow.compile()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "fb69dbb9-91ee-4868-8c3c-93af3cd885be",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "---RETRIEVE---\n",
+ "\"Node 'retrieve':\"\n",
+ "'\\n---\\n'\n",
+ "---CHECK DOCUMENT RELEVANCE TO QUESTION---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---GRADE: DOCUMENT RELEVANT---\n",
+ "---GRADE: DOCUMENT NOT RELEVANT---\n",
+ "---GRADE: DOCUMENT NOT RELEVANT---\n",
+ "---ASSESS GRADED DOCUMENTS---\n",
+ "---DECISION: GENERATE---\n",
+ "\"Node 'grade_documents':\"\n",
+ "'\\n---\\n'\n",
+ "---GENERATE---\n",
+ "---CHECK HALLUCINATIONS---\n",
+ "---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\n",
+ "---GRADE GENERATION vs QUESTION---\n",
+ "---DECISION: GENERATION ADDRESSES QUESTION---\n",
+ "\"Node 'generate':\"\n",
+ "'\\n---\\n'\n",
+ "'Daniel Craig stars as 007 in \"Skyfall\" (2012) and \"Spectre\" (2015).'\n"
+ ]
+ }
+ ],
+ "source": [
+ "from pprint import pprint\n",
+ "\n",
+ "# Run\n",
+ "inputs = {\"question\": \"Movies that star Daniel Craig\"}\n",
+ "for output in app.stream(inputs):\n",
+ " for key, value in output.items():\n",
+ " # Node\n",
+ " pprint(f\"Node '{key}':\")\n",
+ " pprint(\"\\n---\\n\")\n",
+ "\n",
+ "# Final generation\n",
+ "pprint(value[\"generation\"])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4138bc51-8c84-4b8a-8d24-f7f470721f6f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "inputs = {\"question\": \"Which movies are about aliens?\"}\n",
+ "for output in app.stream(inputs):\n",
+ " for key, value in output.items():\n",
+ " # Node\n",
+ " pprint(f\"Node '{key}':\")\n",
+ " pprint(\"\\n---\\n\")\n",
+ "\n",
+ "# Final generation\n",
+ "pprint(value[\"generation\"])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "42369ab8-322d-434a-b5dd-2266e4cb2903",
+ "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.4"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/langgraph/source/examples/react-agent-from-scratch.ipynb b/langgraph/source/examples/react-agent-from-scratch.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..37e605ceef4e514597478c27e74de5a84965fb79
--- /dev/null
+++ b/langgraph/source/examples/react-agent-from-scratch.ipynb
@@ -0,0 +1,40 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "294995c4",
+ "metadata": {},
+ "source": [
+ "[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/how-tos/react-agent-from-scratch.ipynb)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "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.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/langgraph/source/examples/react-agent-structured-output.ipynb b/langgraph/source/examples/react-agent-structured-output.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..8a4bd9b7fdeeeb430739fa36811e1c130c2ff640
--- /dev/null
+++ b/langgraph/source/examples/react-agent-structured-output.ipynb
@@ -0,0 +1,40 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "40f0d107",
+ "metadata": {},
+ "source": [
+ "[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/how-tos/react-agent-structured-output.ipynb)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
+ ]
+ }
+ ],
+ "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.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/langgraph/source/examples/reflection/reflection.ipynb b/langgraph/source/examples/reflection/reflection.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..63fea05d0fe6e1ff715a6d3a79503b041c42ff97
--- /dev/null
+++ b/langgraph/source/examples/reflection/reflection.ipynb
@@ -0,0 +1,41 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "658773a2",
+ "metadata": {},
+ "source": [
+ "[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/reflection/reflection.ipynb)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1cb60657",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
+ ]
+ }
+ ],
+ "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.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/langgraph/source/examples/reflexion/reflexion.ipynb b/langgraph/source/examples/reflexion/reflexion.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..481395dd92643aed7ab69d39e7548140a949410d
--- /dev/null
+++ b/langgraph/source/examples/reflexion/reflexion.ipynb
@@ -0,0 +1,41 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "caf07859",
+ "metadata": {},
+ "source": [
+ "[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/reflexion/reflexion.ipynb)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cd1df0e0",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
+ ]
+ }
+ ],
+ "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.2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/langgraph/source/examples/rewoo/rewoo.ipynb b/langgraph/source/examples/rewoo/rewoo.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..add5406e260d11ed76eb9f3d9b72324ec7458b78
--- /dev/null
+++ b/langgraph/source/examples/rewoo/rewoo.ipynb
@@ -0,0 +1,41 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "961f43ec",
+ "metadata": {},
+ "source": [
+ "[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/rewoo/rewoo.ipynb)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7f00c427",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
+ ]
+ }
+ ],
+ "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": 5
+}
diff --git a/langgraph/source/examples/run-id-langsmith.ipynb b/langgraph/source/examples/run-id-langsmith.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..19f46a167f7912600a4fb176dd65192f75b715b3
--- /dev/null
+++ b/langgraph/source/examples/run-id-langsmith.ipynb
@@ -0,0 +1,40 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "bbd6e9b8",
+ "metadata": {},
+ "source": [
+ "[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/how-tos/run-id-langsmith.md)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
+ ]
+ }
+ ],
+ "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.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/langgraph/source/examples/self-discover/self-discover.ipynb b/langgraph/source/examples/self-discover/self-discover.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..ccdf3b72258f0def6c8a448aa349eaf8414ba44e
--- /dev/null
+++ b/langgraph/source/examples/self-discover/self-discover.ipynb
@@ -0,0 +1,41 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "f6db1873",
+ "metadata": {},
+ "source": [
+ "[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/self-discover/self-discover.ipynb)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "219a78f9",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
+ ]
+ }
+ ],
+ "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.2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/langgraph/source/examples/subgraph.ipynb b/langgraph/source/examples/subgraph.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..d9822a8bf8233cb2fe14b1f0da3ccb80d4b31dbf
--- /dev/null
+++ b/langgraph/source/examples/subgraph.ipynb
@@ -0,0 +1,40 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "f49876e1",
+ "metadata": {},
+ "source": [
+ "[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/how-tos/subgraph.md)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
+ ]
+ }
+ ],
+ "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.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/langgraph/source/examples/tool-calling.ipynb b/langgraph/source/examples/tool-calling.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..ccec5fec47df9423babe2d63c1f48d5e1223fe76
--- /dev/null
+++ b/langgraph/source/examples/tool-calling.ipynb
@@ -0,0 +1,40 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "7fd8bd65",
+ "metadata": {},
+ "source": [
+ "[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/how-tos/tool-calling.md)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
+ ]
+ }
+ ],
+ "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.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/langgraph/source/examples/tutorials/sql-agent.ipynb b/langgraph/source/examples/tutorials/sql-agent.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..b8ce442f7b2c23b464581ae1d4a3c0131fef393a
--- /dev/null
+++ b/langgraph/source/examples/tutorials/sql-agent.ipynb
@@ -0,0 +1,41 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "83c2223f",
+ "metadata": {},
+ "source": [
+ "[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/sql/sql-agent.md)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "57f924b1",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
+ ]
+ }
+ ],
+ "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.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/langgraph/source/examples/tutorials/tnt-llm/tnt-llm.ipynb b/langgraph/source/examples/tutorials/tnt-llm/tnt-llm.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..5c562ac1015ff274599eb051d4b61d9190e9ba09
--- /dev/null
+++ b/langgraph/source/examples/tutorials/tnt-llm/tnt-llm.ipynb
@@ -0,0 +1,41 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "11140167",
+ "metadata": {},
+ "source": [
+ "[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/tnt-llm/tnt-llm.ipynb)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1a2ba3e6",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
+ ]
+ }
+ ],
+ "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": 5
+}
diff --git a/langgraph/source/examples/usaco/usaco.ipynb b/langgraph/source/examples/usaco/usaco.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..f4667a77c6ca6852355d0bd9cf49ab0858beb52b
--- /dev/null
+++ b/langgraph/source/examples/usaco/usaco.ipynb
@@ -0,0 +1,41 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "9dffdb54",
+ "metadata": {},
+ "source": [
+ "[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/usaco/usaco.ipynb)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "579c9959",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
+ ]
+ }
+ ],
+ "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": 5
+}
diff --git a/langgraph/source/examples/web-navigation/web_voyager.ipynb b/langgraph/source/examples/web-navigation/web_voyager.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..28990d51c1490354b427114e011813bda422c87a
--- /dev/null
+++ b/langgraph/source/examples/web-navigation/web_voyager.ipynb
@@ -0,0 +1,41 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "007ea2e9",
+ "metadata": {},
+ "source": [
+ "[This file has been moved](https://github.com/langchain-ai/langgraph/blob/23961cff61a42b52525f3b20b4094d8d2fba1744/docs/docs/tutorials/web-navigation/web_voyager.ipynb)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f0d7b895",
+ "metadata": {},
+ "source": [
+ "This directory is retained purely for archival purposes and is no longer updated. The examples previously found here have been moved to the newly [consolidated LangChain documentation](https://docs.langchain.com/oss/python/langgraph/overview)."
+ ]
+ }
+ ],
+ "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": 5
+}
diff --git a/langgraph/source/libs/__init__.py b/langgraph/source/libs/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/checkpoint-postgres/LICENSE b/langgraph/source/libs/checkpoint-postgres/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..fc0602feecdd6748623c852ab534e1ca612673c7
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 LangChain, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/langgraph/source/libs/checkpoint-postgres/Makefile b/langgraph/source/libs/checkpoint-postgres/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..1116d5daf4aafa2928a0ab3e2858e329dd059249
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/Makefile
@@ -0,0 +1,66 @@
+.PHONY: test test_watch lint format
+
+######################
+# TESTING AND COVERAGE
+######################
+
+start-postgres:
+ 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
+
+POSTGRES_VERSIONS ?= 15 16
+test_pg_version:
+ @echo "Testing PostgreSQL $(POSTGRES_VERSION)"
+ @POSTGRES_VERSION=$(POSTGRES_VERSION) make start-postgres
+ @uv run pytest $(TEST)
+ @EXIT_CODE=$$?; \
+ make stop-postgres; \
+ echo "Finished testing PostgreSQL $(POSTGRES_VERSION); Exit code: $$EXIT_CODE"; \
+ exit $$EXIT_CODE
+
+test:
+ @for version in $(POSTGRES_VERSIONS); do \
+ if ! make test_pg_version POSTGRES_VERSION=$$version; then \
+ echo "Test failed for PostgreSQL $$version"; \
+ exit 1; \
+ fi; \
+ done
+ @echo "All PostgreSQL versions tested successfully"
+
+TEST ?= .
+test_watch:
+ POSTGRES_VERSION=${POSTGRES_VERSION:-16} make start-postgres; \
+ uv run ptw $(TEST); \
+ EXIT_CODE=$$?; \
+ make stop-postgres; \
+ exit $$EXIT_CODE
+
+######################
+# LINTING AND FORMATTING
+######################
+
+# Define a variable for Python and notebook files.
+PYTHON_FILES=.
+MYPY_CACHE=.mypy_cache
+lint format: PYTHON_FILES=.
+lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --relative --diff-filter=d main . | grep -E '\.py$$|\.ipynb$$')
+lint_package: PYTHON_FILES=langgraph
+lint_tests: PYTHON_FILES=tests
+lint_tests: MYPY_CACHE=.mypy_cache_test
+
+lint lint_diff lint_package lint_tests:
+ uv run ruff check .
+ [ "$(PYTHON_FILES)" = "" ] || uv run ruff format $(PYTHON_FILES) --diff
+ [ "$(PYTHON_FILES)" = "" ] || uv run ruff check --select I $(PYTHON_FILES)
+ [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE)
+ [ "$(PYTHON_FILES)" = "" ] || uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
+
+format format_diff:
+ uv run ruff format $(PYTHON_FILES)
+ uv run ruff check --select I --fix $(PYTHON_FILES)
diff --git a/langgraph/source/libs/checkpoint-postgres/README.md b/langgraph/source/libs/checkpoint-postgres/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..c36a60c6032db5004aba3ff5d3b4c983ee813cc2
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/README.md
@@ -0,0 +1,115 @@
+# LangGraph Checkpoint Postgres
+
+Implementation of LangGraph CheckpointSaver that uses Postgres.
+
+## Dependencies
+
+By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) without any extras. However, you can choose a specific installation that best suits your needs [here](https://www.psycopg.org/psycopg3/docs/basic/install.html) (for example, `psycopg[binary]`).
+
+## Usage
+
+> [!IMPORTANT]
+> When using Postgres checkpointers for the first time, make sure to call `.setup()` method on them to create required tables. See example below.
+
+> [!IMPORTANT]
+> When manually creating Postgres connections and passing them to `PostgresSaver` or `AsyncPostgresSaver`, make sure to include `autocommit=True` and `row_factory=dict_row` (`from psycopg.rows import dict_row`). See a full example in this [how-to guide](https://langchain-ai.github.io/langgraph/how-tos/persistence_postgres/).
+>
+> **Why these parameters are required:**
+> - `autocommit=True`: Required for the `.setup()` method to properly commit the checkpoint tables to the database. Without this, table creation may not be persisted.
+> - `row_factory=dict_row`: Required because the PostgresSaver implementation accesses database rows using dictionary-style syntax (e.g., `row["column_name"]`). The default `tuple_row` factory returns tuples that only support index-based access (e.g., `row[0]`), which will cause `TypeError` exceptions when the checkpointer tries to access columns by name.
+>
+> **Example of incorrect usage:**
+> ```python
+> # ❌ This will fail with TypeError during checkpointer operations
+> with psycopg.connect(DB_URI) as conn: # Missing autocommit=True and row_factory=dict_row
+> checkpointer = PostgresSaver(conn)
+> checkpointer.setup() # May not persist tables properly
+> # Any operation that reads from database will fail with:
+> # TypeError: tuple indices must be integers or slices, not str
+> ```
+
+```python
+from langgraph.checkpoint.postgres import PostgresSaver
+
+write_config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
+read_config = {"configurable": {"thread_id": "1"}}
+
+DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
+with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
+ # call .setup() the first time you're using the checkpointer
+ checkpointer.setup()
+ checkpoint = {
+ "v": 4,
+ "ts": "2024-07-31T20:14:19.804150+00:00",
+ "id": "1ef4f797-8335-6428-8001-8a1503f9b875",
+ "channel_values": {
+ "my_key": "meow",
+ "node": "node"
+ },
+ "channel_versions": {
+ "__start__": 2,
+ "my_key": 3,
+ "start:node": 3,
+ "node": 3
+ },
+ "versions_seen": {
+ "__input__": {},
+ "__start__": {
+ "__start__": 1
+ },
+ "node": {
+ "start:node": 2
+ }
+ },
+ }
+
+ # store checkpoint
+ checkpointer.put(write_config, checkpoint, {}, {})
+
+ # load checkpoint
+ checkpointer.get(read_config)
+
+ # list checkpoints
+ list(checkpointer.list(read_config))
+```
+
+### Async
+
+```python
+from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
+
+async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
+ checkpoint = {
+ "v": 4,
+ "ts": "2024-07-31T20:14:19.804150+00:00",
+ "id": "1ef4f797-8335-6428-8001-8a1503f9b875",
+ "channel_values": {
+ "my_key": "meow",
+ "node": "node"
+ },
+ "channel_versions": {
+ "__start__": 2,
+ "my_key": 3,
+ "start:node": 3,
+ "node": 3
+ },
+ "versions_seen": {
+ "__input__": {},
+ "__start__": {
+ "__start__": 1
+ },
+ "node": {
+ "start:node": 2
+ }
+ },
+ }
+
+ # store checkpoint
+ await checkpointer.aput(write_config, checkpoint, {}, {})
+
+ # load checkpoint
+ await checkpointer.aget(read_config)
+
+ # list checkpoints
+ [c async for c in checkpointer.alist(read_config)]
+```
diff --git a/langgraph/source/libs/checkpoint-postgres/__init__.py b/langgraph/source/libs/checkpoint-postgres/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/checkpoint-postgres/langgraph/__init__.py b/langgraph/source/libs/checkpoint-postgres/langgraph/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/langgraph/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/__init__.py b/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py b/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..600127333dafa666873c26eb7a0c9d7950c97d12
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py
@@ -0,0 +1,476 @@
+from __future__ import annotations
+
+import threading
+from collections import defaultdict
+from collections.abc import Iterator, Sequence
+from contextlib import contextmanager
+from typing import Any
+
+from langchain_core.runnables import RunnableConfig
+from langgraph.checkpoint.base import (
+ WRITES_IDX_MAP,
+ ChannelVersions,
+ Checkpoint,
+ CheckpointMetadata,
+ CheckpointTuple,
+ get_checkpoint_id,
+ get_serializable_checkpoint_metadata,
+)
+from langgraph.checkpoint.serde.base import SerializerProtocol
+from psycopg import Capabilities, Connection, Cursor, Pipeline
+from psycopg.rows import DictRow, dict_row
+from psycopg.types.json import Jsonb
+from psycopg_pool import ConnectionPool
+
+from langgraph.checkpoint.postgres import _internal
+from langgraph.checkpoint.postgres.base import BasePostgresSaver
+from langgraph.checkpoint.postgres.shallow import ShallowPostgresSaver
+
+Conn = _internal.Conn # For backward compatibility
+
+
+class PostgresSaver(BasePostgresSaver):
+ """Checkpointer that stores checkpoints in a Postgres database."""
+
+ lock: threading.Lock
+
+ def __init__(
+ self,
+ conn: _internal.Conn,
+ pipe: Pipeline | None = None,
+ serde: SerializerProtocol | None = None,
+ ) -> None:
+ super().__init__(serde=serde)
+ if isinstance(conn, ConnectionPool) and pipe is not None:
+ raise ValueError(
+ "Pipeline should be used only with a single Connection, not ConnectionPool."
+ )
+
+ self.conn = conn
+ self.pipe = pipe
+ self.lock = threading.Lock()
+ self.supports_pipeline = Capabilities().has_pipeline()
+
+ @classmethod
+ @contextmanager
+ def from_conn_string(
+ cls, conn_string: str, *, pipeline: bool = False
+ ) -> Iterator[PostgresSaver]:
+ """Create a new PostgresSaver instance from a connection string.
+
+ Args:
+ conn_string: The Postgres connection info string.
+ pipeline: whether to use Pipeline
+
+ Returns:
+ PostgresSaver: A new PostgresSaver instance.
+ """
+ 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)
+ else:
+ yield cls(conn)
+
+ def setup(self) -> None:
+ """Set up the checkpoint 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 checkpointer is used.
+ """
+ with self._cursor() as cur:
+ 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 :],
+ strict=False,
+ ):
+ cur.execute(migration)
+ cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
+ if self.pipe:
+ self.pipe.sync()
+
+ def list(
+ self,
+ config: RunnableConfig | None,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> Iterator[CheckpointTuple]:
+ """List checkpoints from the database.
+
+ This method retrieves a list of checkpoint tuples from the Postgres database based
+ on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
+
+ Args:
+ config: The config to use for listing the checkpoints.
+ filter: Additional filtering criteria for metadata.
+ before: If provided, only checkpoints before the specified checkpoint ID are returned.
+ limit: The maximum number of checkpoints to return.
+
+ Yields:
+ An iterator of checkpoint tuples.
+
+ Examples:
+ >>> from langgraph.checkpoint.postgres import PostgresSaver
+ >>> DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
+ >>> with PostgresSaver.from_conn_string(DB_URI) as memory:
+ ... # Run a graph, then list the checkpoints
+ >>> config = {"configurable": {"thread_id": "1"}}
+ >>> checkpoints = list(memory.list(config, limit=2))
+ >>> print(checkpoints)
+ [CheckpointTuple(...), CheckpointTuple(...)]
+
+ >>> config = {"configurable": {"thread_id": "1"}}
+ >>> before = {"configurable": {"checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875"}}
+ >>> with PostgresSaver.from_conn_string(DB_URI) as memory:
+ ... # Run a graph, then list the checkpoints
+ >>> checkpoints = list(memory.list(config, before=before))
+ >>> print(checkpoints)
+ [CheckpointTuple(...), ...]
+ """
+ where, args = self._search_where(config, filter, before)
+ query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
+ params = list(args)
+ if limit is not None:
+ query += " LIMIT %s"
+ params.append(int(limit))
+ # if we change this to use .stream() we need to make sure to close the cursor
+ with self._cursor() as cur:
+ cur.execute(query, params)
+ values = cur.fetchall()
+ if not values:
+ return
+ # migrate pending sends if necessary
+ if to_migrate := [
+ v
+ for v in values
+ if v["checkpoint"]["v"] < 4 and v["parent_checkpoint_id"]
+ ]:
+ cur.execute(
+ self.SELECT_PENDING_SENDS_SQL,
+ (
+ values[0]["thread_id"],
+ [v["parent_checkpoint_id"] for v in to_migrate],
+ ),
+ )
+ grouped_by_parent = defaultdict(list)
+ for value in to_migrate:
+ grouped_by_parent[value["parent_checkpoint_id"]].append(value)
+ for sends in cur:
+ for value in grouped_by_parent[sends["checkpoint_id"]]:
+ if value["channel_values"] is None:
+ value["channel_values"] = []
+ self._migrate_pending_sends(
+ sends["sends"],
+ value["checkpoint"],
+ value["channel_values"],
+ )
+ for value in values:
+ yield self._load_checkpoint_tuple(value)
+
+ def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
+ """Get a checkpoint tuple from the database.
+
+ This method retrieves a checkpoint tuple from the Postgres database based on the
+ provided config. If the config contains a `checkpoint_id` key, the checkpoint with
+ the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
+ for the given thread ID is retrieved.
+
+ Args:
+ config: The config to use for retrieving the checkpoint.
+
+ Returns:
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
+
+ Examples:
+
+ Basic:
+ >>> config = {"configurable": {"thread_id": "1"}}
+ >>> checkpoint_tuple = memory.get_tuple(config)
+ >>> print(checkpoint_tuple)
+ CheckpointTuple(...)
+
+ With timestamp:
+
+ >>> config = {
+ ... "configurable": {
+ ... "thread_id": "1",
+ ... "checkpoint_ns": "",
+ ... "checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875",
+ ... }
+ ... }
+ >>> checkpoint_tuple = memory.get_tuple(config)
+ >>> print(checkpoint_tuple)
+ CheckpointTuple(...)
+ """ # noqa
+ thread_id = config["configurable"]["thread_id"]
+ checkpoint_id = get_checkpoint_id(config)
+ checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
+ if checkpoint_id:
+ args: tuple[Any, ...] = (thread_id, checkpoint_ns, checkpoint_id)
+ where = "WHERE thread_id = %s AND checkpoint_ns = %s AND checkpoint_id = %s"
+ else:
+ args = (thread_id, checkpoint_ns)
+ where = "WHERE thread_id = %s AND checkpoint_ns = %s ORDER BY checkpoint_id DESC LIMIT 1"
+
+ with self._cursor() as cur:
+ cur.execute(
+ self.SELECT_SQL + where,
+ args,
+ )
+ value = cur.fetchone()
+ if value is None:
+ return None
+
+ # migrate pending sends if necessary
+ if value["checkpoint"]["v"] < 4 and value["parent_checkpoint_id"]:
+ cur.execute(
+ self.SELECT_PENDING_SENDS_SQL,
+ (thread_id, [value["parent_checkpoint_id"]]),
+ )
+ if sends := cur.fetchone():
+ if value["channel_values"] is None:
+ value["channel_values"] = []
+ self._migrate_pending_sends(
+ sends["sends"],
+ value["checkpoint"],
+ value["channel_values"],
+ )
+
+ return self._load_checkpoint_tuple(value)
+
+ def put(
+ self,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ """Save a checkpoint to the database.
+
+ This method saves a checkpoint to the Postgres database. The checkpoint is associated
+ with the provided config and its parent config (if any).
+
+ Args:
+ config: The config to associate with the checkpoint.
+ checkpoint: The checkpoint to save.
+ metadata: Additional metadata to save with the checkpoint.
+ new_versions: New channel versions as of this write.
+
+ Returns:
+ RunnableConfig: Updated configuration after storing the checkpoint.
+
+ Examples:
+
+ >>> from langgraph.checkpoint.postgres import PostgresSaver
+ >>> DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
+ >>> with PostgresSaver.from_conn_string(DB_URI) as memory:
+ >>> config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
+ >>> checkpoint = {"ts": "2024-05-04T06:32:42.235444+00:00", "id": "1ef4f797-8335-6428-8001-8a1503f9b875", "channel_values": {"key": "value"}}
+ >>> saved_config = memory.put(config, checkpoint, {"source": "input", "step": 1, "writes": {"key": "value"}}, {})
+ >>> print(saved_config)
+ {'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef4f797-8335-6428-8001-8a1503f9b875'}}
+ """
+ configurable = config["configurable"].copy()
+ thread_id = configurable.pop("thread_id")
+ checkpoint_ns = configurable.pop("checkpoint_ns")
+ checkpoint_id = configurable.pop("checkpoint_id", None)
+ copy = checkpoint.copy()
+ copy["channel_values"] = copy["channel_values"].copy()
+ next_config = {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": checkpoint["id"],
+ }
+ }
+
+ # inline primitive values in checkpoint table
+ # others are stored in blobs table
+ blob_values = {}
+ for k, v in checkpoint["channel_values"].items():
+ if v is None or isinstance(v, (str, int, float, bool)):
+ pass
+ else:
+ blob_values[k] = copy["channel_values"].pop(k)
+
+ with self._cursor(pipeline=True) as cur:
+ if blob_versions := {
+ k: v for k, v in new_versions.items() if k in blob_values
+ }:
+ cur.executemany(
+ self.UPSERT_CHECKPOINT_BLOBS_SQL,
+ self._dump_blobs(
+ thread_id,
+ checkpoint_ns,
+ blob_values,
+ blob_versions,
+ ),
+ )
+ cur.execute(
+ self.UPSERT_CHECKPOINTS_SQL,
+ (
+ thread_id,
+ checkpoint_ns,
+ checkpoint["id"],
+ checkpoint_id,
+ Jsonb(copy),
+ Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
+ ),
+ )
+ return next_config
+
+ def put_writes(
+ self,
+ config: RunnableConfig,
+ writes: Sequence[tuple[str, Any]],
+ task_id: str,
+ task_path: str = "",
+ ) -> None:
+ """Store intermediate writes linked to a checkpoint.
+
+ This method saves intermediate writes associated with a checkpoint to the Postgres database.
+
+ Args:
+ config: Configuration of the related checkpoint.
+ writes: List of writes to store.
+ task_id: Identifier for the task creating the writes.
+ """
+ query = (
+ self.UPSERT_CHECKPOINT_WRITES_SQL
+ if all(w[0] in WRITES_IDX_MAP for w in writes)
+ else self.INSERT_CHECKPOINT_WRITES_SQL
+ )
+ with self._cursor(pipeline=True) as cur:
+ cur.executemany(
+ query,
+ self._dump_writes(
+ config["configurable"]["thread_id"],
+ config["configurable"]["checkpoint_ns"],
+ config["configurable"]["checkpoint_id"],
+ task_id,
+ task_path,
+ writes,
+ ),
+ )
+
+ def delete_thread(self, thread_id: str) -> None:
+ """Delete all checkpoints and writes associated with a thread ID.
+
+ Args:
+ thread_id: The thread ID to delete.
+
+ Returns:
+ None
+ """
+ with self._cursor(pipeline=True) as cur:
+ cur.execute(
+ "DELETE FROM checkpoints WHERE thread_id = %s",
+ (str(thread_id),),
+ )
+ cur.execute(
+ "DELETE FROM checkpoint_blobs WHERE thread_id = %s",
+ (str(thread_id),),
+ )
+ cur.execute(
+ "DELETE FROM checkpoint_writes WHERE thread_id = %s",
+ (str(thread_id),),
+ )
+
+ @contextmanager
+ def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor[DictRow]]:
+ """Create a database cursor as a context manager.
+
+ Args:
+ pipeline: whether to use pipeline for the DB operations inside the context manager.
+ 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 self.lock, _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 (
+ 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 (
+ 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 _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
+ """
+ Convert a database row into a CheckpointTuple object.
+
+ Args:
+ value: A row from the database containing checkpoint data.
+
+ Returns:
+ CheckpointTuple: A structured representation of the checkpoint,
+ including its configuration, metadata, parent checkpoint (if any),
+ and pending writes.
+ """
+ return CheckpointTuple(
+ {
+ "configurable": {
+ "thread_id": value["thread_id"],
+ "checkpoint_ns": value["checkpoint_ns"],
+ "checkpoint_id": value["checkpoint_id"],
+ }
+ },
+ {
+ **value["checkpoint"],
+ "channel_values": {
+ **(value["checkpoint"].get("channel_values") or {}),
+ **self._load_blobs(value["channel_values"]),
+ },
+ },
+ value["metadata"],
+ (
+ {
+ "configurable": {
+ "thread_id": value["thread_id"],
+ "checkpoint_ns": value["checkpoint_ns"],
+ "checkpoint_id": value["parent_checkpoint_id"],
+ }
+ }
+ if value["parent_checkpoint_id"]
+ else None
+ ),
+ self._load_writes(value["pending_writes"]),
+ )
+
+
+__all__ = ["PostgresSaver", "BasePostgresSaver", "ShallowPostgresSaver", "Conn"]
diff --git a/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_ainternal.py b/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_ainternal.py
new file mode 100644
index 0000000000000000000000000000000000000000..9339a5623d264c64c7688a0d21ca38e5d84d87b9
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_ainternal.py
@@ -0,0 +1,23 @@
+"""Shared async utility functions for the Postgres checkpoint & storage classes."""
+
+from collections.abc import AsyncIterator
+from contextlib import asynccontextmanager
+
+from psycopg import AsyncConnection
+from psycopg.rows import DictRow
+from psycopg_pool import AsyncConnectionPool
+
+Conn = 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/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_internal.py b/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_internal.py
new file mode 100644
index 0000000000000000000000000000000000000000..77988444ca067dcf52aa0d758d04abe0dac45115
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/_internal.py
@@ -0,0 +1,21 @@
+"""Shared utility functions for the Postgres checkpoint & storage classes."""
+
+from collections.abc import Iterator
+from contextlib import contextmanager
+
+from psycopg import Connection
+from psycopg.rows import DictRow
+from psycopg_pool import ConnectionPool
+
+Conn = 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/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
new file mode 100644
index 0000000000000000000000000000000000000000..5cd533f8db3ac37d75d53fb5c75dad2d3596d06c
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py
@@ -0,0 +1,582 @@
+from __future__ import annotations
+
+import asyncio
+from collections import defaultdict
+from collections.abc import AsyncIterator, Iterator, Sequence
+from contextlib import asynccontextmanager
+from typing import Any
+
+from langchain_core.runnables import RunnableConfig
+from langgraph.checkpoint.base import (
+ WRITES_IDX_MAP,
+ ChannelVersions,
+ Checkpoint,
+ CheckpointMetadata,
+ CheckpointTuple,
+ get_checkpoint_id,
+ get_serializable_checkpoint_metadata,
+)
+from langgraph.checkpoint.serde.base import SerializerProtocol
+from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
+from psycopg.rows import DictRow, dict_row
+from psycopg.types.json import Jsonb
+from psycopg_pool import AsyncConnectionPool
+
+from langgraph.checkpoint.postgres import _ainternal
+from langgraph.checkpoint.postgres.base import BasePostgresSaver
+from langgraph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver
+
+Conn = _ainternal.Conn # For backward compatibility
+
+
+class AsyncPostgresSaver(BasePostgresSaver):
+ """Asynchronous checkpointer that stores checkpoints in a Postgres database."""
+
+ lock: asyncio.Lock
+
+ def __init__(
+ self,
+ conn: _ainternal.Conn,
+ pipe: AsyncPipeline | None = None,
+ serde: SerializerProtocol | None = None,
+ ) -> None:
+ super().__init__(serde=serde)
+ if isinstance(conn, AsyncConnectionPool) and pipe is not None:
+ raise ValueError(
+ "Pipeline should be used only with a single AsyncConnection, not AsyncConnectionPool."
+ )
+
+ self.conn = conn
+ self.pipe = pipe
+ self.lock = asyncio.Lock()
+ self.loop = asyncio.get_running_loop()
+ self.supports_pipeline = Capabilities().has_pipeline()
+
+ @classmethod
+ @asynccontextmanager
+ async def from_conn_string(
+ cls,
+ conn_string: str,
+ *,
+ pipeline: bool = False,
+ serde: SerializerProtocol | None = None,
+ ) -> AsyncIterator[AsyncPostgresSaver]:
+ """Create a new AsyncPostgresSaver instance from a connection string.
+
+ Args:
+ conn_string: The Postgres connection info string.
+ pipeline: whether to use AsyncPipeline
+
+ Returns:
+ AsyncPostgresSaver: A new AsyncPostgresSaver instance.
+ """
+ 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, serde=serde)
+ else:
+ yield cls(conn=conn, serde=serde)
+
+ async def setup(self) -> None:
+ """Set up the checkpoint 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 checkpointer is used.
+ """
+ async with self._cursor() as cur:
+ 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 :],
+ strict=False,
+ ):
+ await cur.execute(migration)
+ await cur.execute(
+ "INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
+ )
+ if self.pipe:
+ await self.pipe.sync()
+
+ async def alist(
+ self,
+ config: RunnableConfig | None,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> AsyncIterator[CheckpointTuple]:
+ """List checkpoints from the database asynchronously.
+
+ This method retrieves a list of checkpoint tuples from the Postgres database based
+ on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
+
+ Args:
+ config: Base configuration for filtering checkpoints.
+ filter: Additional filtering criteria for metadata.
+ before: If provided, only checkpoints before the specified checkpoint ID are returned.
+ limit: Maximum number of checkpoints to return.
+
+ Yields:
+ An asynchronous iterator of matching checkpoint tuples.
+ """
+ where, args = self._search_where(config, filter, before)
+ query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
+ params = list(args)
+ if limit is not None:
+ query += " LIMIT %s"
+ params.append(int(limit))
+ # if we change this to use .stream() we need to make sure to close the cursor
+ async with self._cursor() as cur:
+ await cur.execute(query, params, binary=True)
+ values = await cur.fetchall()
+ if not values:
+ return
+ # migrate pending sends if necessary
+ if to_migrate := [
+ v
+ for v in values
+ if v["checkpoint"]["v"] < 4 and v["parent_checkpoint_id"]
+ ]:
+ await cur.execute(
+ self.SELECT_PENDING_SENDS_SQL,
+ (
+ values[0]["thread_id"],
+ [v["parent_checkpoint_id"] for v in to_migrate],
+ ),
+ )
+ grouped_by_parent = defaultdict(list)
+ for value in to_migrate:
+ grouped_by_parent[value["parent_checkpoint_id"]].append(value)
+ async for sends in cur:
+ for value in grouped_by_parent[sends["checkpoint_id"]]:
+ if value["channel_values"] is None:
+ value["channel_values"] = []
+ self._migrate_pending_sends(
+ sends["sends"],
+ value["checkpoint"],
+ value["channel_values"],
+ )
+ for value in values:
+ yield await self._load_checkpoint_tuple(value)
+
+ async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
+ """Get a checkpoint tuple from the database asynchronously.
+
+ This method retrieves a checkpoint tuple from the Postgres database based on the
+ provided config. If the config contains a `checkpoint_id` key, the checkpoint with
+ the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
+ for the given thread ID is retrieved.
+
+ Args:
+ config: The config to use for retrieving the checkpoint.
+
+ Returns:
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
+ """
+ thread_id = config["configurable"]["thread_id"]
+ checkpoint_id = get_checkpoint_id(config)
+ checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
+ if checkpoint_id:
+ args: tuple[Any, ...] = (thread_id, checkpoint_ns, checkpoint_id)
+ where = "WHERE thread_id = %s AND checkpoint_ns = %s AND checkpoint_id = %s"
+ else:
+ args = (thread_id, checkpoint_ns)
+ where = "WHERE thread_id = %s AND checkpoint_ns = %s ORDER BY checkpoint_id DESC LIMIT 1"
+
+ async with self._cursor() as cur:
+ await cur.execute(
+ self.SELECT_SQL + where,
+ args,
+ binary=True,
+ )
+ value = await cur.fetchone()
+ if value is None:
+ return None
+
+ # migrate pending sends if necessary
+ if value["checkpoint"]["v"] < 4 and value["parent_checkpoint_id"]:
+ await cur.execute(
+ self.SELECT_PENDING_SENDS_SQL,
+ (thread_id, [value["parent_checkpoint_id"]]),
+ )
+ if sends := await cur.fetchone():
+ if value["channel_values"] is None:
+ value["channel_values"] = []
+ self._migrate_pending_sends(
+ sends["sends"],
+ value["checkpoint"],
+ value["channel_values"],
+ )
+
+ return await self._load_checkpoint_tuple(value)
+
+ async def aput(
+ self,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ """Save a checkpoint to the database asynchronously.
+
+ This method saves a checkpoint to the Postgres database. The checkpoint is associated
+ with the provided config and its parent config (if any).
+
+ Args:
+ config: The config to associate with the checkpoint.
+ checkpoint: The checkpoint to save.
+ metadata: Additional metadata to save with the checkpoint.
+ new_versions: New channel versions as of this write.
+
+ Returns:
+ RunnableConfig: Updated configuration after storing the checkpoint.
+ """
+ configurable = config["configurable"].copy()
+ thread_id = configurable.pop("thread_id")
+ checkpoint_ns = configurable.pop("checkpoint_ns")
+ checkpoint_id = configurable.pop("checkpoint_id", None)
+
+ copy = checkpoint.copy()
+ copy["channel_values"] = copy["channel_values"].copy()
+ next_config = {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": checkpoint["id"],
+ }
+ }
+
+ # inline primitive values in checkpoint table
+ # others are stored in blobs table
+ blob_values = {}
+ for k, v in checkpoint["channel_values"].items():
+ if v is None or isinstance(v, (str, int, float, bool)):
+ pass
+ else:
+ blob_values[k] = copy["channel_values"].pop(k)
+
+ async with self._cursor(pipeline=True) as cur:
+ if blob_versions := {
+ k: v for k, v in new_versions.items() if k in blob_values
+ }:
+ await cur.executemany(
+ self.UPSERT_CHECKPOINT_BLOBS_SQL,
+ await asyncio.to_thread(
+ self._dump_blobs,
+ thread_id,
+ checkpoint_ns,
+ blob_values,
+ blob_versions,
+ ),
+ )
+ await cur.execute(
+ self.UPSERT_CHECKPOINTS_SQL,
+ (
+ thread_id,
+ checkpoint_ns,
+ checkpoint["id"],
+ checkpoint_id,
+ Jsonb(copy),
+ Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
+ ),
+ )
+ return next_config
+
+ async def aput_writes(
+ self,
+ config: RunnableConfig,
+ writes: Sequence[tuple[str, Any]],
+ task_id: str,
+ task_path: str = "",
+ ) -> None:
+ """Store intermediate writes linked to a checkpoint asynchronously.
+
+ This method saves intermediate writes associated with a checkpoint to the database.
+
+ Args:
+ config: Configuration of the related checkpoint.
+ writes: List of writes to store, each as (channel, value) pair.
+ task_id: Identifier for the task creating the writes.
+ """
+ query = (
+ self.UPSERT_CHECKPOINT_WRITES_SQL
+ if all(w[0] in WRITES_IDX_MAP for w in writes)
+ else self.INSERT_CHECKPOINT_WRITES_SQL
+ )
+ params = await asyncio.to_thread(
+ self._dump_writes,
+ config["configurable"]["thread_id"],
+ config["configurable"]["checkpoint_ns"],
+ config["configurable"]["checkpoint_id"],
+ task_id,
+ task_path,
+ writes,
+ )
+ async with self._cursor(pipeline=True) as cur:
+ await cur.executemany(query, params)
+
+ async def adelete_thread(self, thread_id: str) -> None:
+ """Delete all checkpoints and writes associated with a thread ID.
+
+ Args:
+ thread_id: The thread ID to delete.
+
+ Returns:
+ None
+ """
+ async with self._cursor(pipeline=True) as cur:
+ await cur.execute(
+ "DELETE FROM checkpoints WHERE thread_id = %s",
+ (str(thread_id),),
+ )
+ await cur.execute(
+ "DELETE FROM checkpoint_blobs WHERE thread_id = %s",
+ (str(thread_id),),
+ )
+ await cur.execute(
+ "DELETE FROM checkpoint_writes WHERE thread_id = %s",
+ (str(thread_id),),
+ )
+
+ @asynccontextmanager
+ async def _cursor(
+ self, *, pipeline: bool = False
+ ) -> AsyncIterator[AsyncCursor[DictRow]]:
+ """Create a database cursor as a context manager.
+
+ Args:
+ pipeline: whether to use pipeline for the DB operations inside the context manager.
+ 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 self.lock, _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:
+ 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 (
+ 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 (
+ conn.transaction(),
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
+ ):
+ yield cur
+ else:
+ async with conn.cursor(binary=True, row_factory=dict_row) as cur:
+ yield cur
+
+ async def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
+ """
+ Convert a database row into a CheckpointTuple object.
+
+ Args:
+ value: A row from the database containing checkpoint data.
+
+ Returns:
+ CheckpointTuple: A structured representation of the checkpoint,
+ including its configuration, metadata, parent checkpoint (if any),
+ and pending writes.
+ """
+ return CheckpointTuple(
+ {
+ "configurable": {
+ "thread_id": value["thread_id"],
+ "checkpoint_ns": value["checkpoint_ns"],
+ "checkpoint_id": value["checkpoint_id"],
+ }
+ },
+ {
+ **value["checkpoint"],
+ "channel_values": {
+ **(value["checkpoint"].get("channel_values") or {}),
+ **self._load_blobs(value["channel_values"]),
+ },
+ },
+ value["metadata"],
+ (
+ {
+ "configurable": {
+ "thread_id": value["thread_id"],
+ "checkpoint_ns": value["checkpoint_ns"],
+ "checkpoint_id": value["parent_checkpoint_id"],
+ }
+ }
+ if value["parent_checkpoint_id"]
+ else None
+ ),
+ await asyncio.to_thread(self._load_writes, value["pending_writes"]),
+ )
+
+ def list(
+ self,
+ config: RunnableConfig | None,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> Iterator[CheckpointTuple]:
+ """List checkpoints from the database.
+
+ This method retrieves a list of checkpoint tuples from the Postgres database based
+ on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
+
+ Args:
+ config: Base configuration for filtering checkpoints.
+ filter: Additional filtering criteria for metadata.
+ before: If provided, only checkpoints before the specified checkpoint ID are returned.
+ limit: Maximum number of checkpoints to return.
+
+ Yields:
+ An iterator of matching checkpoint tuples.
+ """
+ try:
+ # check if we are in the main thread, only bg threads can block
+ # we don't check in other methods to avoid the overhead
+ if asyncio.get_running_loop() is self.loop:
+ raise asyncio.InvalidStateError(
+ "Synchronous calls to AsyncPostgresSaver are only allowed from a "
+ "different thread. From the main thread, use the async interface. "
+ "For example, use `checkpointer.alist(...)` or `await "
+ "graph.ainvoke(...)`."
+ )
+ except RuntimeError:
+ pass
+ aiter_ = self.alist(config, filter=filter, before=before, limit=limit)
+ while True:
+ try:
+ yield asyncio.run_coroutine_threadsafe(
+ anext(aiter_), # type: ignore[arg-type] # noqa: F821
+ self.loop,
+ ).result()
+ except StopAsyncIteration:
+ break
+
+ def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
+ """Get a checkpoint tuple from the database.
+
+ This method retrieves a checkpoint tuple from the Postgres database based on the
+ provided config. If the config contains a `checkpoint_id` key, the checkpoint with
+ the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
+ for the given thread ID is retrieved.
+
+ Args:
+ config: The config to use for retrieving the checkpoint.
+
+ Returns:
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
+ """
+ try:
+ # check if we are in the main thread, only bg threads can block
+ # we don't check in other methods to avoid the overhead
+ if asyncio.get_running_loop() is self.loop:
+ raise asyncio.InvalidStateError(
+ "Synchronous calls to AsyncPostgresSaver are only allowed from a "
+ "different thread. From the main thread, use the async interface. "
+ "For example, use `await checkpointer.aget_tuple(...)` or `await "
+ "graph.ainvoke(...)`."
+ )
+ except RuntimeError:
+ pass
+ return asyncio.run_coroutine_threadsafe(
+ self.aget_tuple(config), self.loop
+ ).result()
+
+ def put(
+ self,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ """Save a checkpoint to the database.
+
+ This method saves a checkpoint to the Postgres database. The checkpoint is associated
+ with the provided config and its parent config (if any).
+
+ Args:
+ config: The config to associate with the checkpoint.
+ checkpoint: The checkpoint to save.
+ metadata: Additional metadata to save with the checkpoint.
+ new_versions: New channel versions as of this write.
+
+ Returns:
+ RunnableConfig: Updated configuration after storing the checkpoint.
+ """
+ return asyncio.run_coroutine_threadsafe(
+ self.aput(config, checkpoint, metadata, new_versions), self.loop
+ ).result()
+
+ def put_writes(
+ self,
+ config: RunnableConfig,
+ writes: Sequence[tuple[str, Any]],
+ task_id: str,
+ task_path: str = "",
+ ) -> None:
+ """Store intermediate writes linked to a checkpoint.
+
+ This method saves intermediate writes associated with a checkpoint to the database.
+
+ Args:
+ config: Configuration of the related checkpoint.
+ writes: List of writes to store, each as (channel, value) pair.
+ task_id: Identifier for the task creating the writes.
+ task_path: Path of the task creating the writes.
+ """
+ return asyncio.run_coroutine_threadsafe(
+ self.aput_writes(config, writes, task_id, task_path), self.loop
+ ).result()
+
+ def delete_thread(self, thread_id: str) -> None:
+ """Delete all checkpoints and writes associated with a thread ID.
+
+ Args:
+ thread_id: The thread ID to delete.
+
+ Returns:
+ None
+ """
+ try:
+ # check if we are in the main thread, only bg threads can block
+ # we don't check in other methods to avoid the overhead
+ if asyncio.get_running_loop() is self.loop:
+ raise asyncio.InvalidStateError(
+ "Synchronous calls to AsyncPostgresSaver are only allowed from a "
+ "different thread. From the main thread, use the async interface. "
+ "For example, use `await checkpointer.aget_tuple(...)` or `await "
+ "graph.ainvoke(...)`."
+ )
+ except RuntimeError:
+ pass
+ return asyncio.run_coroutine_threadsafe(
+ self.adelete_thread(thread_id), self.loop
+ ).result()
+
+
+__all__ = ["AsyncPostgresSaver", "AsyncShallowPostgresSaver", "Conn"]
diff --git a/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py b/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f27fee5b538c18e19feed8a7db6ffa59e1765e3
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py
@@ -0,0 +1,315 @@
+from __future__ import annotations
+
+import random
+import warnings
+from collections.abc import Sequence
+from importlib.metadata import version as get_version
+from typing import Any, cast
+
+from langchain_core.runnables import RunnableConfig
+from langgraph.checkpoint.base import (
+ WRITES_IDX_MAP,
+ BaseCheckpointSaver,
+ ChannelVersions,
+ get_checkpoint_id,
+)
+from langgraph.checkpoint.serde.types import TASKS
+from psycopg.types.json import Jsonb
+
+MetadataInput = dict[str, Any] | None
+
+try:
+ major, minor = get_version("langgraph").split(".")[:2]
+ if int(major) == 0 and int(minor) < 5:
+ warnings.warn(
+ "You're using incompatible versions of langgraph and checkpoint-postgres. Please upgrade langgraph to avoid unexpected behavior.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+except Exception:
+ # skip version check if running from source
+ pass
+
+"""
+To add a new migration, add a new string to the MIGRATIONS list.
+The position of the migration in the list is the version number.
+"""
+MIGRATIONS = [
+ """CREATE TABLE IF NOT EXISTS checkpoint_migrations (
+ v INTEGER PRIMARY KEY
+);""",
+ """CREATE TABLE IF NOT EXISTS checkpoints (
+ thread_id TEXT NOT NULL,
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
+ checkpoint_id TEXT NOT NULL,
+ parent_checkpoint_id TEXT,
+ type TEXT,
+ checkpoint JSONB NOT NULL,
+ metadata JSONB NOT NULL DEFAULT '{}',
+ PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
+);""",
+ """CREATE TABLE IF NOT EXISTS checkpoint_blobs (
+ thread_id TEXT NOT NULL,
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
+ channel TEXT NOT NULL,
+ version TEXT NOT NULL,
+ type TEXT NOT NULL,
+ blob BYTEA,
+ PRIMARY KEY (thread_id, checkpoint_ns, channel, version)
+);""",
+ """CREATE TABLE IF NOT EXISTS checkpoint_writes (
+ thread_id TEXT NOT NULL,
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
+ checkpoint_id TEXT NOT NULL,
+ task_id TEXT NOT NULL,
+ idx INTEGER NOT NULL,
+ channel TEXT NOT NULL,
+ type TEXT,
+ blob BYTEA NOT NULL,
+ PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
+);""",
+ "ALTER TABLE checkpoint_blobs ALTER COLUMN blob DROP not null;",
+ # NOTE: this is a no-op migration to ensure that the versions in the migrations table are correct.
+ # This is necessary due to an empty migration previously added to the list.
+ "SELECT 1;",
+ """
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoints_thread_id_idx ON checkpoints(thread_id);
+ """,
+ """
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_blobs_thread_id_idx ON checkpoint_blobs(thread_id);
+ """,
+ """
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
+ """,
+ """ALTER TABLE checkpoint_writes ADD COLUMN IF NOT EXISTS task_path TEXT NOT NULL DEFAULT '';""",
+]
+
+SELECT_SQL = """
+select
+ thread_id,
+ checkpoint,
+ checkpoint_ns,
+ checkpoint_id,
+ parent_checkpoint_id,
+ metadata,
+ (
+ select array_agg(array[bl.channel::bytea, bl.type::bytea, bl.blob])
+ from jsonb_each_text(checkpoint -> 'channel_versions')
+ inner join checkpoint_blobs bl
+ on bl.thread_id = checkpoints.thread_id
+ and bl.checkpoint_ns = checkpoints.checkpoint_ns
+ and bl.channel = jsonb_each_text.key
+ and bl.version = jsonb_each_text.value
+ ) as channel_values,
+ (
+ select
+ array_agg(array[cw.task_id::text::bytea, cw.channel::bytea, cw.type::bytea, cw.blob] order by cw.task_id, cw.idx)
+ from checkpoint_writes cw
+ where cw.thread_id = checkpoints.thread_id
+ and cw.checkpoint_ns = checkpoints.checkpoint_ns
+ and cw.checkpoint_id = checkpoints.checkpoint_id
+ ) as pending_writes
+from checkpoints """
+
+SELECT_PENDING_SENDS_SQL = f"""
+select
+ checkpoint_id,
+ array_agg(array[type::bytea, blob] order by task_path, task_id, idx) as sends
+from checkpoint_writes
+where thread_id = %s
+ and checkpoint_id = any(%s)
+ and channel = '{TASKS}'
+group by checkpoint_id
+"""
+
+UPSERT_CHECKPOINT_BLOBS_SQL = """
+ INSERT INTO checkpoint_blobs (thread_id, checkpoint_ns, channel, version, type, blob)
+ VALUES (%s, %s, %s, %s, %s, %s)
+ ON CONFLICT (thread_id, checkpoint_ns, channel, version) DO NOTHING
+"""
+
+UPSERT_CHECKPOINTS_SQL = """
+ INSERT INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, checkpoint, metadata)
+ VALUES (%s, %s, %s, %s, %s, %s)
+ ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id)
+ DO UPDATE SET
+ checkpoint = EXCLUDED.checkpoint,
+ metadata = EXCLUDED.metadata;
+"""
+
+UPSERT_CHECKPOINT_WRITES_SQL = """
+ INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, task_path, idx, channel, type, blob)
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
+ ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO UPDATE SET
+ channel = EXCLUDED.channel,
+ type = EXCLUDED.type,
+ blob = EXCLUDED.blob;
+"""
+
+INSERT_CHECKPOINT_WRITES_SQL = """
+ INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, task_path, idx, channel, type, blob)
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
+ ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO NOTHING
+"""
+
+
+class BasePostgresSaver(BaseCheckpointSaver[str]):
+ SELECT_SQL = SELECT_SQL
+ SELECT_PENDING_SENDS_SQL = SELECT_PENDING_SENDS_SQL
+ MIGRATIONS = MIGRATIONS
+ UPSERT_CHECKPOINT_BLOBS_SQL = UPSERT_CHECKPOINT_BLOBS_SQL
+ UPSERT_CHECKPOINTS_SQL = UPSERT_CHECKPOINTS_SQL
+ UPSERT_CHECKPOINT_WRITES_SQL = UPSERT_CHECKPOINT_WRITES_SQL
+ INSERT_CHECKPOINT_WRITES_SQL = INSERT_CHECKPOINT_WRITES_SQL
+
+ supports_pipeline: bool
+
+ def _migrate_pending_sends(
+ self,
+ pending_sends: list[tuple[bytes, bytes]],
+ checkpoint: dict[str, Any],
+ channel_values: list[tuple[bytes, bytes, bytes]],
+ ) -> None:
+ if not pending_sends:
+ return
+ # add to values
+ enc, blob = self.serde.dumps_typed(
+ [self.serde.loads_typed((c.decode(), b)) for c, b in pending_sends],
+ )
+ channel_values.append((TASKS.encode(), enc.encode(), blob))
+ # add to versions
+ checkpoint["channel_versions"][TASKS] = (
+ max(checkpoint["channel_versions"].values())
+ if checkpoint["channel_versions"]
+ else self.get_next_version(None, None)
+ )
+
+ def _load_blobs(
+ self, blob_values: list[tuple[bytes, bytes, bytes]]
+ ) -> dict[str, Any]:
+ if not blob_values:
+ return {}
+ return {
+ k.decode(): self.serde.loads_typed((t.decode(), v))
+ for k, t, v in blob_values
+ if t.decode() != "empty"
+ }
+
+ def _dump_blobs(
+ self,
+ thread_id: str,
+ checkpoint_ns: str,
+ values: dict[str, Any],
+ versions: ChannelVersions,
+ ) -> list[tuple[str, str, str, str, str, bytes | None]]:
+ if not versions:
+ return []
+
+ return [
+ (
+ thread_id,
+ checkpoint_ns,
+ k,
+ cast(str, ver),
+ *(
+ self.serde.dumps_typed(values[k])
+ if k in values
+ else ("empty", None)
+ ),
+ )
+ for k, ver in versions.items()
+ ]
+
+ def _load_writes(
+ self, writes: list[tuple[bytes, bytes, bytes, bytes]]
+ ) -> list[tuple[str, str, Any]]:
+ return (
+ [
+ (
+ tid.decode(),
+ channel.decode(),
+ self.serde.loads_typed((t.decode(), v)),
+ )
+ for tid, channel, t, v in writes
+ ]
+ if writes
+ else []
+ )
+
+ def _dump_writes(
+ self,
+ thread_id: str,
+ checkpoint_ns: str,
+ checkpoint_id: str,
+ task_id: str,
+ task_path: str,
+ writes: Sequence[tuple[str, Any]],
+ ) -> list[tuple[str, str, str, str, str, int, str, str, bytes]]:
+ return [
+ (
+ thread_id,
+ checkpoint_ns,
+ checkpoint_id,
+ task_id,
+ task_path,
+ WRITES_IDX_MAP.get(channel, idx),
+ channel,
+ *self.serde.dumps_typed(value),
+ )
+ for idx, (channel, value) in enumerate(writes)
+ ]
+
+ def get_next_version(self, current: str | None, channel: None) -> str:
+ if current is None:
+ current_v = 0
+ elif isinstance(current, int):
+ current_v = current
+ else:
+ current_v = int(current.split(".")[0])
+ next_v = current_v + 1
+ next_h = random.random()
+ return f"{next_v:032}.{next_h:016}"
+
+ def _search_where(
+ self,
+ config: RunnableConfig | None,
+ filter: MetadataInput,
+ before: RunnableConfig | None = None,
+ ) -> 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
+ is the parametered WHERE clause predicate (including the WHERE keyword):
+ "WHERE column1 = $1 AND column2 IS $2". The list of values contains the
+ values for each of the corresponding parameters.
+ """
+ wheres = []
+ param_values = []
+
+ # construct predicate for config filter
+ if config:
+ wheres.append("thread_id = %s ")
+ param_values.append(config["configurable"]["thread_id"])
+ checkpoint_ns = config["configurable"].get("checkpoint_ns")
+ if checkpoint_ns is not None:
+ wheres.append("checkpoint_ns = %s")
+ param_values.append(checkpoint_ns)
+
+ if checkpoint_id := get_checkpoint_id(config):
+ wheres.append("checkpoint_id = %s ")
+ param_values.append(checkpoint_id)
+
+ # construct predicate for metadata filter
+ if filter:
+ wheres.append("metadata @> %s ")
+ param_values.append(Jsonb(filter))
+
+ # construct predicate for `before`
+ if before is not None:
+ wheres.append("checkpoint_id < %s ")
+ param_values.append(get_checkpoint_id(before))
+
+ return (
+ "WHERE " + " AND ".join(wheres) if wheres else "",
+ param_values,
+ )
diff --git a/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/py.typed b/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/py.typed
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/shallow.py b/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/shallow.py
new file mode 100644
index 0000000000000000000000000000000000000000..90fc95e740166504e6021d898c73e78877677989
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/langgraph/checkpoint/postgres/shallow.py
@@ -0,0 +1,967 @@
+import asyncio
+import threading
+import warnings
+from collections.abc import AsyncIterator, Iterator, Sequence
+from contextlib import asynccontextmanager, contextmanager
+from typing import Any
+
+from langchain_core.runnables import RunnableConfig
+from langgraph.checkpoint.base import (
+ WRITES_IDX_MAP,
+ ChannelVersions,
+ Checkpoint,
+ CheckpointMetadata,
+ CheckpointTuple,
+ get_serializable_checkpoint_metadata,
+)
+from langgraph.checkpoint.serde.base import SerializerProtocol
+from langgraph.checkpoint.serde.types import TASKS
+from psycopg import (
+ AsyncConnection,
+ AsyncCursor,
+ AsyncPipeline,
+ Capabilities,
+ Connection,
+ Cursor,
+ Pipeline,
+)
+from psycopg.rows import DictRow, dict_row
+from psycopg.types.json import Jsonb
+from psycopg_pool import AsyncConnectionPool, ConnectionPool
+
+from langgraph.checkpoint.postgres import _ainternal, _internal
+from langgraph.checkpoint.postgres.base import BasePostgresSaver
+
+"""
+To add a new migration, add a new string to the MIGRATIONS list.
+The position of the migration in the list is the version number.
+"""
+MIGRATIONS = [
+ """CREATE TABLE IF NOT EXISTS checkpoint_migrations (
+ v INTEGER PRIMARY KEY
+);""",
+ """CREATE TABLE IF NOT EXISTS checkpoints (
+ thread_id TEXT NOT NULL,
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
+ type TEXT,
+ checkpoint JSONB NOT NULL,
+ metadata JSONB NOT NULL DEFAULT '{}',
+ PRIMARY KEY (thread_id, checkpoint_ns)
+);""",
+ """CREATE TABLE IF NOT EXISTS checkpoint_blobs (
+ thread_id TEXT NOT NULL,
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
+ channel TEXT NOT NULL,
+ type TEXT NOT NULL,
+ blob BYTEA,
+ PRIMARY KEY (thread_id, checkpoint_ns, channel)
+);""",
+ """CREATE TABLE IF NOT EXISTS checkpoint_writes (
+ thread_id TEXT NOT NULL,
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
+ checkpoint_id TEXT NOT NULL,
+ task_id TEXT NOT NULL,
+ idx INTEGER NOT NULL,
+ channel TEXT NOT NULL,
+ type TEXT,
+ blob BYTEA NOT NULL,
+ PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
+);""",
+ """
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoints_thread_id_idx ON checkpoints(thread_id);
+ """,
+ """
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_blobs_thread_id_idx ON checkpoint_blobs(thread_id);
+ """,
+ """
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
+ """,
+ """
+ ALTER TABLE checkpoint_writes ADD COLUMN IF NOT EXISTS task_path TEXT NOT NULL DEFAULT '';
+ """,
+]
+
+SELECT_SQL = f"""
+select
+ thread_id,
+ checkpoint,
+ checkpoint_ns,
+ metadata,
+ (
+ select array_agg(array[bl.channel::bytea, bl.type::bytea, bl.blob])
+ from jsonb_each_text(checkpoint -> 'channel_versions')
+ inner join checkpoint_blobs bl
+ on bl.thread_id = checkpoints.thread_id
+ and bl.checkpoint_ns = checkpoints.checkpoint_ns
+ and bl.channel = jsonb_each_text.key
+ ) as channel_values,
+ (
+ select
+ array_agg(array[cw.task_id::text::bytea, cw.channel::bytea, cw.type::bytea, cw.blob] order by cw.task_id, cw.idx)
+ from checkpoint_writes cw
+ where cw.thread_id = checkpoints.thread_id
+ and cw.checkpoint_ns = checkpoints.checkpoint_ns
+ and cw.checkpoint_id = (checkpoint->>'id')
+ ) as pending_writes,
+ (
+ select array_agg(array[cw.type::bytea, cw.blob] order by cw.task_path, cw.task_id, cw.idx)
+ from checkpoint_writes cw
+ where cw.thread_id = checkpoints.thread_id
+ and cw.checkpoint_ns = checkpoints.checkpoint_ns
+ and cw.channel = '{TASKS}'
+ ) as pending_sends
+from checkpoints """
+
+UPSERT_CHECKPOINT_BLOBS_SQL = """
+ INSERT INTO checkpoint_blobs (thread_id, checkpoint_ns, channel, type, blob)
+ VALUES (%s, %s, %s, %s, %s)
+ ON CONFLICT (thread_id, checkpoint_ns, channel) DO UPDATE SET
+ type = EXCLUDED.type,
+ blob = EXCLUDED.blob;
+"""
+
+UPSERT_CHECKPOINTS_SQL = """
+ INSERT INTO checkpoints (thread_id, checkpoint_ns, checkpoint, metadata)
+ VALUES (%s, %s, %s, %s)
+ ON CONFLICT (thread_id, checkpoint_ns)
+ DO UPDATE SET
+ checkpoint = EXCLUDED.checkpoint,
+ metadata = EXCLUDED.metadata;
+"""
+
+UPSERT_CHECKPOINT_WRITES_SQL = """
+ INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, task_path, idx, channel, type, blob)
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
+ ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO UPDATE SET
+ channel = EXCLUDED.channel,
+ type = EXCLUDED.type,
+ blob = EXCLUDED.blob;
+"""
+
+INSERT_CHECKPOINT_WRITES_SQL = """
+ INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, task_path, idx, channel, type, blob)
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
+ ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO NOTHING
+"""
+
+
+def _dump_blobs(
+ serde: SerializerProtocol,
+ thread_id: str,
+ checkpoint_ns: str,
+ values: dict[str, Any],
+ versions: ChannelVersions,
+) -> list[tuple[str, str, str, str, bytes | None]]:
+ if not versions:
+ return []
+
+ return [
+ (
+ thread_id,
+ checkpoint_ns,
+ k,
+ *(serde.dumps_typed(values[k]) if k in values else ("empty", None)),
+ )
+ for k in versions
+ ]
+
+
+class ShallowPostgresSaver(BasePostgresSaver):
+ """A checkpoint saver that uses Postgres to store checkpoints.
+
+ This checkpointer ONLY stores the most recent checkpoint and does NOT retain any history.
+ It is meant to be a light-weight drop-in replacement for the PostgresSaver that
+ supports most of the LangGraph persistence functionality with the exception of time travel.
+ """
+
+ SELECT_SQL = SELECT_SQL
+ MIGRATIONS = MIGRATIONS
+ UPSERT_CHECKPOINT_BLOBS_SQL = UPSERT_CHECKPOINT_BLOBS_SQL
+ UPSERT_CHECKPOINTS_SQL = UPSERT_CHECKPOINTS_SQL
+ UPSERT_CHECKPOINT_WRITES_SQL = UPSERT_CHECKPOINT_WRITES_SQL
+ INSERT_CHECKPOINT_WRITES_SQL = INSERT_CHECKPOINT_WRITES_SQL
+
+ lock: threading.Lock
+
+ def __init__(
+ self,
+ conn: _internal.Conn,
+ pipe: Pipeline | None = None,
+ serde: SerializerProtocol | None = None,
+ ) -> None:
+ warnings.warn(
+ "ShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
+ "Use PostgresSaver instead, and invoke the graph with `graph.invoke(..., durability='exit')`.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ super().__init__(serde=serde)
+ if isinstance(conn, ConnectionPool) and pipe is not None:
+ raise ValueError(
+ "Pipeline should be used only with a single Connection, not ConnectionPool."
+ )
+
+ self.conn = conn
+ self.pipe = pipe
+ self.lock = threading.Lock()
+ self.supports_pipeline = Capabilities().has_pipeline()
+
+ @classmethod
+ @contextmanager
+ def from_conn_string(
+ cls, conn_string: str, *, pipeline: bool = False
+ ) -> Iterator["ShallowPostgresSaver"]:
+ """Create a new ShallowPostgresSaver instance from a connection string.
+
+ Args:
+ conn_string: The Postgres connection info string.
+ pipeline: whether to use Pipeline
+
+ Returns:
+ ShallowPostgresSaver: A new ShallowPostgresSaver instance.
+ """
+ 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)
+ else:
+ yield cls(conn)
+
+ def setup(self) -> None:
+ """Set up the checkpoint 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 checkpointer is used.
+ """
+ with self._cursor() as cur:
+ 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 :],
+ strict=False,
+ ):
+ cur.execute(migration)
+ cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
+ if self.pipe:
+ self.pipe.sync()
+
+ def list(
+ self,
+ config: RunnableConfig | None,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> Iterator[CheckpointTuple]:
+ """List checkpoints from the database.
+
+ This method retrieves a list of checkpoint tuples from the Postgres database based
+ on the provided config. For ShallowPostgresSaver, this method returns a list with
+ ONLY the most recent checkpoint.
+ """
+ where, args = self._search_where(config, filter, before)
+ query = self.SELECT_SQL + where
+ params = list(args)
+ if limit is not None:
+ query += " LIMIT %s"
+ params.append(int(limit))
+ with self._cursor() as cur:
+ cur.execute(query, params, binary=True)
+ for value in cur:
+ checkpoint: Checkpoint = {
+ **value["checkpoint"],
+ "channel_values": self._load_blobs(value["channel_values"]),
+ "pending_sends": [
+ self.serde.loads_typed((t.decode(), v))
+ for t, v in value["pending_sends"]
+ ]
+ if value["pending_sends"]
+ else [],
+ }
+ yield CheckpointTuple(
+ config={
+ "configurable": {
+ "thread_id": value["thread_id"],
+ "checkpoint_ns": value["checkpoint_ns"],
+ "checkpoint_id": checkpoint["id"],
+ }
+ },
+ checkpoint=checkpoint,
+ metadata=value["metadata"],
+ pending_writes=self._load_writes(value["pending_writes"]),
+ )
+
+ def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
+ """Get a checkpoint tuple from the database.
+
+ This method retrieves a checkpoint tuple from the Postgres database based on the
+ provided config (matching the thread ID in the config).
+
+ Args:
+ config: The config to use for retrieving the checkpoint.
+
+ Returns:
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
+
+ Examples:
+
+ Basic:
+ >>> config = {"configurable": {"thread_id": "1"}}
+ >>> checkpoint_tuple = memory.get_tuple(config)
+ >>> print(checkpoint_tuple)
+ CheckpointTuple(...)
+
+ With timestamp:
+
+ >>> config = {
+ ... "configurable": {
+ ... "thread_id": "1",
+ ... "checkpoint_ns": "",
+ ... "checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875",
+ ... }
+ ... }
+ >>> checkpoint_tuple = memory.get_tuple(config)
+ >>> print(checkpoint_tuple)
+ CheckpointTuple(...)
+ """ # noqa
+ thread_id = config["configurable"]["thread_id"]
+ checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
+ args = (thread_id, checkpoint_ns)
+ where = "WHERE thread_id = %s AND checkpoint_ns = %s"
+
+ with self._cursor() as cur:
+ cur.execute(
+ self.SELECT_SQL + where,
+ args,
+ binary=True,
+ )
+
+ for value in cur:
+ checkpoint: Checkpoint = {
+ **value["checkpoint"],
+ "channel_values": self._load_blobs(value["channel_values"]),
+ "pending_sends": [
+ self.serde.loads_typed((t.decode(), v))
+ for t, v in value["pending_sends"]
+ ]
+ if value["pending_sends"]
+ else [],
+ }
+ return CheckpointTuple(
+ config={
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": checkpoint["id"],
+ }
+ },
+ checkpoint=checkpoint,
+ metadata=value["metadata"],
+ pending_writes=self._load_writes(value["pending_writes"]),
+ )
+
+ def put(
+ self,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ """Save a checkpoint to the database.
+
+ This method saves a checkpoint to the Postgres database. The checkpoint is associated
+ with the provided config. For ShallowPostgresSaver, this method saves ONLY the most recent
+ checkpoint and overwrites a previous checkpoint, if it exists.
+
+ Args:
+ config: The config to associate with the checkpoint.
+ checkpoint: The checkpoint to save.
+ metadata: Additional metadata to save with the checkpoint.
+ new_versions: New channel versions as of this write.
+
+ Returns:
+ RunnableConfig: Updated configuration after storing the checkpoint.
+
+ Examples:
+
+ >>> from langgraph.checkpoint.postgres import ShallowPostgresSaver
+ >>> DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
+ >>> with ShallowPostgresSaver.from_conn_string(DB_URI) as memory:
+ >>> config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
+ >>> checkpoint = {"ts": "2024-05-04T06:32:42.235444+00:00", "id": "1ef4f797-8335-6428-8001-8a1503f9b875", "channel_values": {"key": "value"}}
+ >>> saved_config = memory.put(config, checkpoint, {"source": "input", "step": 1, "writes": {"key": "value"}}, {})
+ >>> print(saved_config)
+ {'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef4f797-8335-6428-8001-8a1503f9b875'}}
+ """
+ configurable = config["configurable"].copy()
+ thread_id = configurable.pop("thread_id")
+ checkpoint_ns = configurable.pop("checkpoint_ns")
+
+ copy = checkpoint.copy()
+ next_config = {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": checkpoint["id"],
+ }
+ }
+
+ with self._cursor(pipeline=True) as cur:
+ cur.execute(
+ """DELETE FROM checkpoint_writes
+ WHERE thread_id = %s AND checkpoint_ns = %s AND checkpoint_id NOT IN (%s, %s)""",
+ (
+ thread_id,
+ checkpoint_ns,
+ checkpoint["id"],
+ configurable.get("checkpoint_id", ""),
+ ),
+ )
+ cur.executemany(
+ self.UPSERT_CHECKPOINT_BLOBS_SQL,
+ _dump_blobs(
+ self.serde,
+ thread_id,
+ checkpoint_ns,
+ copy.pop("channel_values"), # type: ignore[misc]
+ new_versions,
+ ),
+ )
+ cur.execute(
+ self.UPSERT_CHECKPOINTS_SQL,
+ (
+ thread_id,
+ checkpoint_ns,
+ Jsonb(copy),
+ Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
+ ),
+ )
+ return next_config
+
+ def put_writes(
+ self,
+ config: RunnableConfig,
+ writes: Sequence[tuple[str, Any]],
+ task_id: str,
+ task_path: str = "",
+ ) -> None:
+ """Store intermediate writes linked to a checkpoint.
+
+ This method saves intermediate writes associated with a checkpoint to the Postgres database.
+
+ Args:
+ config: Configuration of the related checkpoint.
+ writes: List of writes to store.
+ task_id: Identifier for the task creating the writes.
+ """
+ query = (
+ self.UPSERT_CHECKPOINT_WRITES_SQL
+ if all(w[0] in WRITES_IDX_MAP for w in writes)
+ else self.INSERT_CHECKPOINT_WRITES_SQL
+ )
+ with self._cursor(pipeline=True) as cur:
+ cur.executemany(
+ query,
+ self._dump_writes(
+ config["configurable"]["thread_id"],
+ config["configurable"]["checkpoint_ns"],
+ config["configurable"]["checkpoint_id"],
+ task_id,
+ task_path,
+ writes,
+ ),
+ )
+
+ @contextmanager
+ def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor[DictRow]]:
+ """Create a database cursor as a context manager.
+
+ Args:
+ pipeline: whether to use pipeline for the DB operations inside the context manager.
+ Will be applied regardless of whether the ShallowPostgresSaver instance was initialized with a pipeline.
+ If pipeline mode is not supported, will fall back to using transaction context manager.
+ """
+ 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
+ # 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:
+ # 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,
+ ):
+ yield cur
+ else:
+ with self.lock, conn.cursor(binary=True, row_factory=dict_row) as cur:
+ yield cur
+
+
+class AsyncShallowPostgresSaver(BasePostgresSaver):
+ """A checkpoint saver that uses Postgres to store checkpoints asynchronously.
+
+ This checkpointer ONLY stores the most recent checkpoint and does NOT retain any history.
+ It is meant to be a light-weight drop-in replacement for the AsyncPostgresSaver that
+ supports most of the LangGraph persistence functionality with the exception of time travel.
+ """
+
+ SELECT_SQL = SELECT_SQL
+ MIGRATIONS = MIGRATIONS
+ UPSERT_CHECKPOINT_BLOBS_SQL = UPSERT_CHECKPOINT_BLOBS_SQL
+ UPSERT_CHECKPOINTS_SQL = UPSERT_CHECKPOINTS_SQL
+ UPSERT_CHECKPOINT_WRITES_SQL = UPSERT_CHECKPOINT_WRITES_SQL
+ INSERT_CHECKPOINT_WRITES_SQL = INSERT_CHECKPOINT_WRITES_SQL
+ lock: asyncio.Lock
+
+ def __init__(
+ self,
+ conn: _ainternal.Conn,
+ pipe: AsyncPipeline | None = None,
+ serde: SerializerProtocol | None = None,
+ ) -> None:
+ warnings.warn(
+ "AsyncShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
+ "Use AsyncPostgresSaver instead, and invoke the graph with `await graph.ainvoke(..., durability='exit')`.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ super().__init__(serde=serde)
+ if isinstance(conn, AsyncConnectionPool) and pipe is not None:
+ raise ValueError(
+ "Pipeline should be used only with a single AsyncConnection, not AsyncConnectionPool."
+ )
+
+ self.conn = conn
+ self.pipe = pipe
+ self.lock = asyncio.Lock()
+ self.loop = asyncio.get_running_loop()
+ self.supports_pipeline = Capabilities().has_pipeline()
+
+ @classmethod
+ @asynccontextmanager
+ async def from_conn_string(
+ cls,
+ conn_string: str,
+ *,
+ pipeline: bool = False,
+ serde: SerializerProtocol | None = None,
+ ) -> AsyncIterator["AsyncShallowPostgresSaver"]:
+ """Create a new AsyncShallowPostgresSaver instance from a connection string.
+
+ Args:
+ conn_string: The Postgres connection info string.
+ pipeline: whether to use AsyncPipeline
+
+ Returns:
+ AsyncShallowPostgresSaver: A new AsyncShallowPostgresSaver instance.
+ """
+ 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, serde=serde)
+ else:
+ yield cls(conn=conn, serde=serde)
+
+ async def setup(self) -> None:
+ """Set up the checkpoint 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 checkpointer is used.
+ """
+ async with self._cursor() as cur:
+ 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 :],
+ strict=False,
+ ):
+ await cur.execute(migration)
+ await cur.execute(
+ "INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
+ )
+ if self.pipe:
+ await self.pipe.sync()
+
+ async def alist(
+ self,
+ config: RunnableConfig | None,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> AsyncIterator[CheckpointTuple]:
+ """List checkpoints from the database asynchronously.
+
+ This method retrieves a list of checkpoint tuples from the Postgres database based
+ on the provided config. For ShallowPostgresSaver, this method returns a list with
+ ONLY the most recent checkpoint.
+ """
+ where, args = self._search_where(config, filter, before)
+ query = self.SELECT_SQL + where
+ params = list(args)
+ if limit is not None:
+ query += " LIMIT %s"
+ params.append(int(limit))
+ async with self._cursor() as cur:
+ await cur.execute(query, params, binary=True)
+ async for value in cur:
+ checkpoint: Checkpoint = {
+ **value["checkpoint"],
+ "channel_values": self._load_blobs(value["channel_values"]),
+ "pending_sends": [
+ self.serde.loads_typed((t.decode(), v))
+ for t, v in value["pending_sends"]
+ ]
+ if value["pending_sends"]
+ else [],
+ }
+ yield CheckpointTuple(
+ config={
+ "configurable": {
+ "thread_id": value["thread_id"],
+ "checkpoint_ns": value["checkpoint_ns"],
+ "checkpoint_id": checkpoint["id"],
+ }
+ },
+ checkpoint=checkpoint,
+ metadata=value["metadata"],
+ pending_writes=await asyncio.to_thread(
+ self._load_writes, value["pending_writes"]
+ ),
+ )
+
+ async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
+ """Get a checkpoint tuple from the database asynchronously.
+
+ This method retrieves a checkpoint tuple from the Postgres database based on the
+ provided config (matching the thread ID in the config).
+
+ Args:
+ config: The config to use for retrieving the checkpoint.
+
+ Returns:
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
+ """
+ thread_id = config["configurable"]["thread_id"]
+ checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
+ args = (thread_id, checkpoint_ns)
+ where = "WHERE thread_id = %s AND checkpoint_ns = %s"
+
+ async with self._cursor() as cur:
+ await cur.execute(
+ self.SELECT_SQL + where,
+ args,
+ binary=True,
+ )
+
+ async for value in cur:
+ checkpoint: Checkpoint = {
+ **value["checkpoint"],
+ "channel_values": self._load_blobs(value["channel_values"]),
+ "pending_sends": [
+ self.serde.loads_typed((t.decode(), v))
+ for t, v in value["pending_sends"]
+ ]
+ if value["pending_sends"]
+ else [],
+ }
+ return CheckpointTuple(
+ config={
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": checkpoint["id"],
+ }
+ },
+ checkpoint=checkpoint,
+ metadata=value["metadata"],
+ pending_writes=await asyncio.to_thread(
+ self._load_writes, value["pending_writes"]
+ ),
+ )
+
+ async def aput(
+ self,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ """Save a checkpoint to the database asynchronously.
+
+ This method saves a checkpoint to the Postgres database. The checkpoint is associated
+ with the provided config. For AsyncShallowPostgresSaver, this method saves ONLY the most recent
+ checkpoint and overwrites a previous checkpoint, if it exists.
+
+ Args:
+ config: The config to associate with the checkpoint.
+ checkpoint: The checkpoint to save.
+ metadata: Additional metadata to save with the checkpoint.
+ new_versions: New channel versions as of this write.
+
+ Returns:
+ RunnableConfig: Updated configuration after storing the checkpoint.
+ """
+ configurable = config["configurable"].copy()
+ thread_id = configurable.pop("thread_id")
+ checkpoint_ns = configurable.pop("checkpoint_ns")
+
+ copy = checkpoint.copy()
+ next_config = {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": checkpoint["id"],
+ }
+ }
+
+ async with self._cursor(pipeline=True) as cur:
+ await cur.execute(
+ """DELETE FROM checkpoint_writes
+ WHERE thread_id = %s AND checkpoint_ns = %s AND checkpoint_id NOT IN (%s, %s)""",
+ (
+ thread_id,
+ checkpoint_ns,
+ checkpoint["id"],
+ configurable.get("checkpoint_id", ""),
+ ),
+ )
+ await cur.executemany(
+ self.UPSERT_CHECKPOINT_BLOBS_SQL,
+ _dump_blobs(
+ self.serde,
+ thread_id,
+ checkpoint_ns,
+ copy.pop("channel_values"), # type: ignore[misc]
+ new_versions,
+ ),
+ )
+ await cur.execute(
+ self.UPSERT_CHECKPOINTS_SQL,
+ (
+ thread_id,
+ checkpoint_ns,
+ Jsonb(copy),
+ Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
+ ),
+ )
+ return next_config
+
+ async def aput_writes(
+ self,
+ config: RunnableConfig,
+ writes: Sequence[tuple[str, Any]],
+ task_id: str,
+ task_path: str = "",
+ ) -> None:
+ """Store intermediate writes linked to a checkpoint asynchronously.
+
+ This method saves intermediate writes associated with a checkpoint to the database.
+
+ Args:
+ config: Configuration of the related checkpoint.
+ writes: List of writes to store, each as (channel, value) pair.
+ task_id: Identifier for the task creating the writes.
+ """
+ query = (
+ self.UPSERT_CHECKPOINT_WRITES_SQL
+ if all(w[0] in WRITES_IDX_MAP for w in writes)
+ else self.INSERT_CHECKPOINT_WRITES_SQL
+ )
+ params = await asyncio.to_thread(
+ self._dump_writes,
+ config["configurable"]["thread_id"],
+ config["configurable"]["checkpoint_ns"],
+ config["configurable"]["checkpoint_id"],
+ task_id,
+ task_path,
+ writes,
+ )
+ async with self._cursor(pipeline=True) as cur:
+ await cur.executemany(query, params)
+
+ @asynccontextmanager
+ async def _cursor(
+ self, *, pipeline: bool = False
+ ) -> AsyncIterator[AsyncCursor[DictRow]]:
+ """Create a database cursor as a context manager.
+
+ Args:
+ pipeline: whether to use pipeline for the DB operations inside the context manager.
+ Will be applied regardless of whether the AsyncShallowPostgresSaver instance was initialized with a pipeline.
+ If pipeline mode is not supported, will fall back to using transaction context manager.
+ """
+ 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:
+ 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, 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,
+ ):
+ yield cur
+ else:
+ async with (
+ self.lock,
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
+ ):
+ yield cur
+
+ def list(
+ self,
+ config: RunnableConfig | None,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> Iterator[CheckpointTuple]:
+ """List checkpoints from the database.
+
+ This method retrieves a list of checkpoint tuples from the Postgres database based
+ on the provided config. For ShallowPostgresSaver, this method returns a list with
+ ONLY the most recent checkpoint.
+ """
+ aiter_ = self.alist(config, filter=filter, before=before, limit=limit)
+ while True:
+ try:
+ yield asyncio.run_coroutine_threadsafe(
+ anext(aiter_), # type: ignore[arg-type] # noqa: F821
+ self.loop,
+ ).result()
+ except StopAsyncIteration:
+ break
+
+ def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
+ """Get a checkpoint tuple from the database.
+
+ This method retrieves a checkpoint tuple from the Postgres database based on the
+ provided config (matching the thread ID in the config).
+
+ Args:
+ config: The config to use for retrieving the checkpoint.
+
+ Returns:
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
+ """
+ try:
+ # check if we are in the main thread, only bg threads can block
+ # we don't check in other methods to avoid the overhead
+ if asyncio.get_running_loop() is self.loop:
+ raise asyncio.InvalidStateError(
+ "Synchronous calls to AsyncShallowPostgresSaver are only allowed from a "
+ "different thread. From the main thread, use the async interface."
+ "For example, use `await checkpointer.aget_tuple(...)` or `await "
+ "graph.ainvoke(...)`."
+ )
+ except RuntimeError:
+ pass
+ return asyncio.run_coroutine_threadsafe(
+ self.aget_tuple(config), self.loop
+ ).result()
+
+ def put(
+ self,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ """Save a checkpoint to the database.
+
+ This method saves a checkpoint to the Postgres database. The checkpoint is associated
+ with the provided config. For AsyncShallowPostgresSaver, this method saves ONLY the most recent
+ checkpoint and overwrites a previous checkpoint, if it exists.
+
+ Args:
+ config: The config to associate with the checkpoint.
+ checkpoint: The checkpoint to save.
+ metadata: Additional metadata to save with the checkpoint.
+ new_versions: New channel versions as of this write.
+
+ Returns:
+ RunnableConfig: Updated configuration after storing the checkpoint.
+ """
+ return asyncio.run_coroutine_threadsafe(
+ self.aput(config, checkpoint, metadata, new_versions), self.loop
+ ).result()
+
+ def put_writes(
+ self,
+ config: RunnableConfig,
+ writes: Sequence[tuple[str, Any]],
+ task_id: str,
+ task_path: str = "",
+ ) -> None:
+ """Store intermediate writes linked to a checkpoint.
+
+ This method saves intermediate writes associated with a checkpoint to the database.
+
+ Args:
+ config: Configuration of the related checkpoint.
+ writes: List of writes to store, each as (channel, value) pair.
+ task_id: Identifier for the task creating the writes.
+ task_path: Path of the task creating the writes.
+ """
+ return asyncio.run_coroutine_threadsafe(
+ self.aput_writes(config, writes, task_id, task_path), self.loop
+ ).result()
diff --git a/langgraph/source/libs/checkpoint-postgres/langgraph/store/__init__.py b/langgraph/source/libs/checkpoint-postgres/langgraph/store/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/langgraph/store/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/checkpoint-postgres/langgraph/store/postgres/__init__.py b/langgraph/source/libs/checkpoint-postgres/langgraph/store/postgres/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..d0432b588b56cddc5b0c7b77c1565612d8f90616
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/langgraph/store/postgres/__init__.py
@@ -0,0 +1,4 @@
+from langgraph.store.postgres.aio import AsyncPostgresStore
+from langgraph.store.postgres.base import PoolConfig, PostgresStore
+
+__all__ = ["AsyncPostgresStore", "PoolConfig", "PostgresStore"]
diff --git a/langgraph/source/libs/checkpoint-postgres/langgraph/store/postgres/aio.py b/langgraph/source/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
new file mode 100644
index 0000000000000000000000000000000000000000..2ccaa833aca4a22317c1b0ac48cc63bb7d4f6724
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/langgraph/store/postgres/aio.py
@@ -0,0 +1,591 @@
+from __future__ import annotations
+
+import asyncio
+import logging
+from collections.abc import AsyncIterator, Callable, Iterable, Sequence
+from contextlib import asynccontextmanager
+from types import TracebackType
+from typing import Any, cast
+
+import orjson
+from langgraph.store.base import (
+ GetOp,
+ ListNamespacesOp,
+ Op,
+ PutOp,
+ Result,
+ SearchOp,
+)
+from langgraph.store.base.batch import AsyncBatchedBaseStore
+from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
+from psycopg.rows import DictRow, dict_row
+from psycopg_pool import AsyncConnectionPool
+
+from langgraph.checkpoint.postgres import _ainternal
+from langgraph.store.postgres.base import (
+ PLACEHOLDER,
+ BasePostgresStore,
+ PoolConfig,
+ PostgresIndexConfig,
+ Row,
+ TTLConfig,
+ _decode_ns_bytes,
+ _ensure_index_config,
+ _group_ops,
+ _row_to_item,
+ _row_to_search_item,
+)
+
+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 usage:
+ ```python
+ from langgraph.store.postgres import AsyncPostgresStore
+
+ conn_string = "postgresql://user:pass@localhost:5432/dbname"
+
+ async with AsyncPostgresStore.from_conn_string(conn_string) as store:
+ await store.setup() # Run migrations. Done once
+
+ # 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
+
+ conn_string = "postgresql://user:pass@localhost:5432/dbname"
+
+ async with AsyncPostgresStore.from_conn_string(
+ conn_string,
+ 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() # Run migrations. Done once
+
+ # Store documents
+ await store.aput(("docs",), "doc1", {"text": "Python tutorial"})
+ await store.aput(("docs",), "doc2", {"text": "TypeScript guide"})
+ await store.aput(("docs",), "doc3", {"text": "Other guide"}, index=False) # don't index
+
+ # Search by similarity
+ results = await store.asearch(("docs",), query="programming guides", limit=2)
+ ```
+
+ Using connection pooling for better performance:
+ ```python
+ from langgraph.store.postgres import AsyncPostgresStore, PoolConfig
+
+ conn_string = "postgresql://user:pass@localhost:5432/dbname"
+
+ async with AsyncPostgresStore.from_conn_string(
+ conn_string,
+ pool_config=PoolConfig(
+ min_size=5,
+ max_size=20
+ )
+ ) as store:
+ await store.setup() # Run migrations. Done once
+ # 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
+
+ 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.
+
+ Note:
+ If you provide a TTL configuration, you must explicitly call `start_ttl_sweeper()` to begin
+ the background task that removes expired items. Call `stop_ttl_sweeper()` to properly
+ clean up resources when you're done with the store.
+ """
+
+ __slots__ = (
+ "_deserializer",
+ "pipe",
+ "lock",
+ "supports_pipeline",
+ "index_config",
+ "embeddings",
+ "ttl_config",
+ "_ttl_sweeper_task",
+ "_ttl_stop_event",
+ )
+ supports_ttl: bool = True
+
+ def __init__(
+ self,
+ conn: _ainternal.Conn,
+ *,
+ pipe: AsyncPipeline | None = None,
+ deserializer: Callable[[bytes | orjson.Fragment], dict[str, Any]] | None = None,
+ index: PostgresIndexConfig | None = None,
+ ttl: TTLConfig | None = 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()
+ self.index_config = index
+ if self.index_config:
+ self.embeddings, self.index_config = _ensure_index_config(self.index_config)
+ else:
+ self.embeddings = None
+
+ self.ttl_config = ttl
+ self._ttl_sweeper_task: asyncio.Task[None] | None = None
+ self._ttl_stop_event = asyncio.Event()
+
+ async def abatch(self, ops: Iterable[Op]) -> list[Result]:
+ grouped_ops, num_ops = _group_ops(ops)
+ results: list[Result] = [None] * num_ops
+
+ 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
+
+ @classmethod
+ @asynccontextmanager
+ async def from_conn_string(
+ cls,
+ conn_string: str,
+ *,
+ pipeline: bool = False,
+ pool_config: PoolConfig | None = None,
+ index: PostgresIndexConfig | None = None,
+ ttl: TTLConfig | None = None,
+ ) -> AsyncIterator[AsyncPostgresStore]:
+ """Create a new AsyncPostgresStore instance from a connection string.
+
+ Args:
+ conn_string: The Postgres connection info string.
+ pipeline: Whether to use AsyncPipeline (only for single connections)
+ pool_config: 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: 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, ttl=ttl)
+ 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, ttl=ttl)
+ else:
+ yield cls(conn=conn, index=index, ttl=ttl)
+
+ 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:
+ await cur.execute(
+ f"""
+ CREATE TABLE IF NOT EXISTS {table} (
+ v INTEGER PRIMARY KEY
+ )
+ """
+ )
+ await cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
+ row = cast(dict, await cur.fetchone())
+ if row is None:
+ version = -1
+ else:
+ version = row["v"]
+ 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()
+ }
+ if "dims" in params:
+ try:
+ params["dims"] = int(params["dims"])
+ except Exception as e:
+ raise ValueError(
+ f"Invalid dims for vector index: {params['dims']}"
+ ) from e
+ if "vector_type" in params:
+ vt = str(params["vector_type"])
+ if vt not in ("vector", "halfvec"):
+ raise ValueError(
+ f"Invalid vector_type for pgvector: {vt}"
+ )
+ params["vector_type"] = vt
+ if "index_type" in params:
+ it = str(params["index_type"])
+ if it not in ("hnsw", "ivfflat"):
+ raise ValueError(
+ f"Invalid index_type for pgvector: {it}"
+ )
+ params["index_type"] = it
+ sql = sql % params
+ await cur.execute(sql)
+ await cur.execute(
+ "INSERT INTO vector_migrations (v) VALUES (%s)", (v,)
+ )
+
+ async def sweep_ttl(self) -> int:
+ """Delete expired store items based on TTL.
+
+ Returns:
+ int: The number of deleted items.
+ """
+ async with self._cursor() as cur:
+ await cur.execute(
+ """
+ DELETE FROM store
+ WHERE expires_at IS NOT NULL AND expires_at < NOW()
+ """
+ )
+ deleted_count = cur.rowcount
+ return deleted_count
+
+ async def start_ttl_sweeper(
+ self, sweep_interval_minutes: int | None = None
+ ) -> asyncio.Task[None]:
+ """Periodically delete expired store items based on TTL.
+
+ Returns:
+ Task that can be awaited or cancelled.
+ """
+ if not self.ttl_config:
+ return asyncio.create_task(asyncio.sleep(0))
+
+ if self._ttl_sweeper_task is not None and not self._ttl_sweeper_task.done():
+ return self._ttl_sweeper_task
+
+ self._ttl_stop_event.clear()
+
+ interval = float(
+ sweep_interval_minutes or self.ttl_config.get("sweep_interval_minutes") or 5
+ )
+ logger.info(f"Starting store TTL sweeper with interval {interval} minutes")
+
+ async def _sweep_loop() -> None:
+ while not self._ttl_stop_event.is_set():
+ try:
+ try:
+ await asyncio.wait_for(
+ self._ttl_stop_event.wait(),
+ timeout=interval * 60,
+ )
+ break
+ except asyncio.TimeoutError:
+ pass
+
+ expired_items = await self.sweep_ttl()
+ if expired_items > 0:
+ logger.info(f"Store swept {expired_items} expired items")
+ except asyncio.CancelledError:
+ break
+ except Exception as exc:
+ logger.exception("Store TTL sweep iteration failed", exc_info=exc)
+
+ task = asyncio.create_task(_sweep_loop())
+ task.set_name("ttl_sweeper")
+ self._ttl_sweeper_task = task
+ return task
+
+ async def stop_ttl_sweeper(self, timeout: float | None = None) -> bool:
+ """Stop the TTL sweeper task if it's running.
+
+ Args:
+ timeout: Maximum time to wait for the task to stop, in seconds.
+ If `None`, wait indefinitely.
+
+ Returns:
+ bool: True if the task was successfully stopped or wasn't running,
+ False if the timeout was reached before the task stopped.
+ """
+ if self._ttl_sweeper_task is None or self._ttl_sweeper_task.done():
+ return True
+
+ logger.info("Stopping TTL sweeper task")
+ self._ttl_stop_event.set()
+
+ if timeout is not None:
+ try:
+ await asyncio.wait_for(self._ttl_sweeper_task, timeout=timeout)
+ success = True
+ except asyncio.TimeoutError:
+ success = False
+ else:
+ await self._ttl_sweeper_task
+ success = True
+
+ if success:
+ self._ttl_sweeper_task = None
+ logger.info("TTL sweeper task stopped")
+ else:
+ logger.warning("Timed out waiting for TTL sweeper task to stop")
+
+ return success
+
+ async def __aenter__(self) -> AsyncPostgresStore:
+ return self
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ # Ensure the TTL sweeper task is stopped when exiting the context
+ if hasattr(self, "_ttl_sweeper_task") and self._ttl_sweeper_task is not None:
+ # Set the event to signal the task to stop
+ self._ttl_stop_event.set()
+ # We don't wait for the task to complete here to avoid blocking
+ # The task will clean up itself gracefully
+
+ async def _execute_batch(
+ self,
+ grouped_ops: dict,
+ results: list[Result],
+ conn: AsyncConnection[DictRow],
+ ) -> None:
+ 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]),
+ 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:
+ for query, params, namespace, items in self._get_batch_GET_ops_queries(get_ops):
+ await cur.execute(query, params)
+ rows = cast(list[Row], await cur.fetchall())
+ key_to_row = {row["key"]: row for row in rows}
+ for idx, key in items:
+ row = key_to_row.get(key)
+ if row:
+ results[idx] = _row_to_item(
+ namespace, row, loader=self._deserializer
+ )
+ else:
+ results[idx] = None
+
+ async def _batch_put_ops(
+ self,
+ put_ops: Sequence[tuple[int, PutOp]],
+ cur: AsyncCursor[DictRow],
+ ) -> None:
+ 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, strict=False
+ )
+ for p in (ns, k, pathname, vector)
+ ],
+ )
+ )
+
+ for query, params in queries:
+ 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, 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, strict=False):
+ _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, strict=False):
+ await cur.execute(query, params)
+ rows = cast(list[Row], await cur.fetchall())
+ items = [
+ _row_to_search_item(
+ _decode_ns_bytes(row["prefix"]), row, loader=self._deserializer
+ )
+ for row in rows
+ ]
+ results[idx] = items
+
+ async def _batch_list_namespaces_ops(
+ self,
+ list_ops: Sequence[tuple[int, ListNamespacesOp]],
+ results: list[Result],
+ cur: AsyncCursor[DictRow],
+ ) -> None:
+ queries = self._get_batch_list_namespaces_queries(list_ops)
+ for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
+ await cur.execute(query, params)
+ 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, *, pipeline: bool = False
+ ) -> AsyncIterator[AsyncCursor[DictRow]]:
+ """Create a database cursor as a context manager.
+
+ Args:
+ 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.
+ """
+ is_pooled_conn = isinstance(self.conn, AsyncConnectionPool)
+ # With AsyncConnectionPool, each _cursor() call checks out its own connection.
+ # The pool does not hand out the same connection concurrently, so a shared lock
+ # across calls is unnecessary here.
+ lock = asyncio.Lock() if is_pooled_conn else self.lock
+ 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:
+ 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 (
+ lock,
+ conn.pipeline(),
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
+ ):
+ yield cur
+ else:
+ async with (
+ lock,
+ conn.transaction(),
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
+ ):
+ yield cur
+ else:
+ async with (
+ lock,
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
+ ):
+ yield cur
diff --git a/langgraph/source/libs/checkpoint-postgres/langgraph/store/postgres/base.py b/langgraph/source/libs/checkpoint-postgres/langgraph/store/postgres/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..0d1cdc8dc8e62a003860f48ee88c498586d2e8c8
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/langgraph/store/postgres/base.py
@@ -0,0 +1,1409 @@
+from __future__ import annotations
+
+import asyncio
+import concurrent.futures
+import json
+import logging
+import threading
+from collections import defaultdict
+from collections.abc import Callable, Iterable, Iterator, Sequence
+from contextlib import contextmanager
+from datetime import datetime
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Generic,
+ Literal,
+ NamedTuple,
+ TypeVar,
+ cast,
+)
+
+import orjson
+from langgraph.store.base import (
+ BaseStore,
+ GetOp,
+ IndexConfig,
+ Item,
+ ListNamespacesOp,
+ Op,
+ PutOp,
+ Result,
+ SearchItem,
+ SearchOp,
+ TTLConfig,
+ ensure_embeddings,
+ get_text_at_path,
+ tokenize_path,
+)
+from psycopg import Capabilities, Connection, Cursor, Pipeline
+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
+
+if TYPE_CHECKING:
+ from langchain_core.embeddings import Embeddings
+
+logger = logging.getLogger(__name__)
+
+
+class Migration(NamedTuple):
+ """A database migration with optional conditions and parameters."""
+
+ sql: str
+ params: dict[str, Any] | None = None
+ condition: Callable[[BasePostgresStore], bool] | None = None
+
+
+MIGRATIONS: Sequence[str] = [
+ """
+CREATE TABLE IF NOT EXISTS store (
+ -- 'prefix' represents the doc's 'namespace'
+ prefix text NOT NULL,
+ key text NOT NULL,
+ value jsonb NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (prefix, key)
+);
+""",
+ """
+-- For faster lookups by prefix
+CREATE INDEX CONCURRENTLY IF NOT EXISTS store_prefix_idx ON store USING btree (prefix text_pattern_ops);
+""",
+ """
+-- Add expires_at column to store table
+ALTER TABLE store
+ADD COLUMN IF NOT EXISTS expires_at TIMESTAMP WITH TIME ZONE,
+ADD COLUMN IF NOT EXISTS ttl_minutes INT;
+""",
+ """
+-- Add indexes for efficient TTL sweeping
+CREATE INDEX IF NOT EXISTS idx_store_expires_at ON store (expires_at)
+WHERE expires_at IS NOT NULL;
+""",
+]
+
+VECTOR_MIGRATIONS: Sequence[Migration] = [
+ Migration(
+ """
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'vector') THEN
+ CREATE EXTENSION vector;
+ END IF;
+END $$;
+""",
+ ),
+ 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")
+ ),
+ },
+ ),
+ Migration(
+ """
+CREATE INDEX CONCURRENTLY 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=_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: int | None
+ """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 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:
+ - 'vector': Regular vectors (default)
+ - 'halfvec': Half-precision vectors for reduced memory usage
+ """
+
+
+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.
+
+ 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: Callable[[bytes | orjson.Fragment], dict[str, Any]] | None
+ index_config: PostgresIndexConfig | None
+
+ def _get_batch_GET_ops_queries(
+ self,
+ get_ops: Sequence[tuple[int, GetOp]],
+ ) -> list[tuple[str, tuple, tuple[str, ...], list]]:
+ """
+ Build queries to fetch (and optionally refresh the TTL of) multiple keys per namespace.
+
+ Each returned element is a tuple of:
+ (sql_query_string, sql_params, namespace, items_for_this_namespace)
+
+ where items_for_this_namespace is the original list of (idx, key, refresh_ttl).
+ """
+
+ namespace_groups = defaultdict(list)
+ refresh_ttls = defaultdict(list)
+ for idx, op in get_ops:
+ namespace_groups[op.namespace].append((idx, op.key))
+ refresh_ttls[op.namespace].append(op.refresh_ttl)
+
+ results = []
+ for namespace, items in namespace_groups.items():
+ _, keys = zip(*items, strict=False)
+ this_refresh_ttls = refresh_ttls[namespace]
+
+ query = """
+ WITH passed_in AS (
+ SELECT unnest(%s::text[]) AS key,
+ unnest(%s::bool[]) AS do_refresh
+ ),
+ updated AS (
+ UPDATE store s
+ SET expires_at = NOW() + (s.ttl_minutes || ' minutes')::interval
+ FROM passed_in p
+ WHERE s.prefix = %s
+ AND s.key = p.key
+ AND p.do_refresh = TRUE
+ AND s.ttl_minutes IS NOT NULL
+ RETURNING s.key
+ )
+ SELECT s.key, s.value, s.created_at, s.updated_at
+ FROM store s
+ JOIN passed_in p ON s.key = p.key
+ WHERE s.prefix = %s
+ """
+ ns_text = _namespace_to_text(namespace)
+ params = (
+ list(keys), # -> unnest(%s::text[])
+ list(this_refresh_ttls), # -> unnest(%s::bool[])
+ ns_text, # -> prefix = %s (for UPDATE)
+ ns_text, # -> prefix = %s (for final SELECT)
+ )
+ results.append((query, params, namespace, items))
+
+ return results
+
+ def _prepare_batch_PUT_queries(
+ self,
+ put_ops: Sequence[tuple[int, PutOp]],
+ ) -> tuple[
+ list[tuple[str, Sequence]],
+ tuple[str, Sequence[tuple[str, str, str, str]]] | None,
+ ]:
+ 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 dedupped_ops.values():
+ if op.value is None:
+ deletes.append(op)
+ else:
+ inserts.append(op)
+
+ queries: list[tuple[str, Sequence]] = []
+
+ if deletes:
+ namespace_groups: dict[tuple[str, ...], list[str]] = defaultdict(list)
+ for op in deletes:
+ namespace_groups[op.namespace].append(op.key)
+ for namespace, keys in namespace_groups.items():
+ placeholders = ",".join(["%s"] * len(keys))
+ query = (
+ f"DELETE FROM store WHERE prefix = %s AND key IN ({placeholders})"
+ )
+ params = (_namespace_to_text(namespace), *keys)
+ queries.append((query, params))
+ embedding_request: tuple[str, Sequence[tuple[str, str, str, str]]] | None = None
+ if inserts:
+ values = []
+ insertion_params: list[Any] = []
+ vector_values = []
+ embedding_request_params = []
+ # Handle TTL expiration
+
+ # First handle main store insertions
+ for op in inserts:
+ insertion_params.extend(
+ (
+ _namespace_to_text(op.namespace),
+ op.key,
+ Jsonb(cast(dict, op.value)),
+ )
+ )
+ if op.ttl is not None:
+ values.append(
+ "(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NOW() + %s::interval, %s)"
+ )
+ ttl_minutes = float(op.ttl)
+ insertion_params.extend(
+ (
+ f"{ttl_minutes * 60} seconds",
+ ttl_minutes,
+ )
+ )
+ else:
+ values.append(
+ "(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, %s)"
+ )
+ insertion_params.append(None)
+
+ # 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 = cast(dict, 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, expires_at, ttl_minutes)
+ VALUES {values_str}
+ ON CONFLICT (prefix, key) DO UPDATE
+ SET value = EXCLUDED.value,
+ updated_at = CURRENT_TIMESTAMP,
+ expires_at = EXCLUDED.expires_at,
+ ttl_minutes = EXCLUDED.ttl_minutes
+ """
+ queries.append((query, insertion_params))
+
+ 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)
+
+ return queries, embedding_request
+
+ def _prepare_batch_search_queries(
+ self,
+ search_ops: Sequence[tuple[int, SearchOp]],
+ ) -> tuple[
+ list[tuple[str, list[None | str | list[float]]]], # queries, params
+ list[tuple[int, str]], # idx, query_text pairs to embed
+ ]:
+ """
+ Build per-SearchOp SQL queries (with optional TTL refresh) plus embedding requests.
+ Returns:
+ - queries: list of (SQL, param_list)
+ - embedding_requests: list of (original_index_in_search_ops, text_query)
+ """
+
+ queries = []
+ embedding_requests = []
+ for idx, (_, op) in enumerate(search_ops):
+ filter_params = []
+ filter_clauses = []
+ if op.filter:
+ for key, value in op.filter.items():
+ if isinstance(value, dict):
+ for op_name, val in value.items():
+ condition, params_ = self._get_filter_condition(
+ key, op_name, val
+ )
+ filter_clauses.append(condition)
+ filter_params.extend(params_)
+ else:
+ filter_clauses.append("value->%s = %s::jsonb")
+ filter_params.extend([key, orjson.dumps(value).decode("utf-8")])
+
+ ns_condition = "TRUE"
+ ns_param: Sequence[str] | None = None
+ if op.namespace_prefix:
+ ns_condition = "store.prefix LIKE %s"
+ ns_param = (f"{_namespace_to_text(op.namespace_prefix)}%",)
+ else:
+ ns_param = ()
+
+ extra_filters = (
+ " AND " + " AND ".join(filter_clauses) if filter_clauses else ""
+ )
+
+ if op.query and self.index_config:
+ # We'll embed the text later, so record the request.
+ embedding_requests.append((idx, op.query))
+
+ score_operator, post_operator = get_distance_operator(self)
+ post_operator = post_operator.replace("scored", "uniq")
+ vector_type = self.index_config.get("ann_index_config", {}).get(
+ "vector_type", "vector"
+ )
+
+ # For hamming bit vectors, or “regular” vectors
+ if (
+ vector_type == "bit"
+ and cast(dict, self.index_config).get("distance_type") == "hamming"
+ ):
+ score_operator = score_operator % (
+ "%s",
+ cast(dict, self.index_config)["dims"],
+ )
+ else:
+ if vector_type not in ("vector", "halfvec"):
+ raise ValueError(
+ f"Invalid vector_type for pgvector: {vector_type}"
+ )
+ score_operator = score_operator % ("%s", vector_type)
+
+ vectors_per_doc_estimate = cast(dict, self.index_config)[
+ "__estimated_num_vectors"
+ ]
+ expanded_limit = (op.limit * vectors_per_doc_estimate * 2) + 1
+
+ # “sub_scored” does the main vector search
+ # Then we do DISTINCT ON to drop duplicates if your store can have them
+ # Finally we limit & offset
+ vector_search_cte = f"""
+ SELECT store.prefix, store.key, store.value, store.created_at, store.updated_at,
+ {score_operator} AS neg_score
+ FROM store
+ JOIN store_vectors sv ON store.prefix = sv.prefix AND store.key = sv.key
+ WHERE {ns_condition} {extra_filters}
+ ORDER BY {score_operator} ASC
+ LIMIT %s
+ """
+
+ search_results_sql = f"""
+ WITH scored AS (
+ {vector_search_cte}
+ )
+ SELECT uniq.prefix, uniq.key, uniq.value, uniq.created_at, uniq.updated_at,
+ {post_operator} AS score
+ FROM (
+ SELECT DISTINCT ON (scored.prefix, scored.key)
+ scored.prefix, scored.key, scored.value, scored.created_at, scored.updated_at, scored.neg_score
+ FROM scored
+ ORDER BY scored.prefix, scored.key, scored.neg_score ASC
+ ) uniq
+ ORDER BY score DESC
+ LIMIT %s
+ OFFSET %s
+ """
+
+ search_results_params = [
+ PLACEHOLDER,
+ *ns_param,
+ *filter_params,
+ PLACEHOLDER,
+ expanded_limit,
+ op.limit,
+ op.offset,
+ ]
+
+ else:
+ base_query = f"""
+ SELECT store.prefix, store.key, store.value, store.created_at, store.updated_at, NULL AS score
+ FROM store
+ WHERE {ns_condition} {extra_filters}
+ ORDER BY store.updated_at DESC
+ LIMIT %s
+ OFFSET %s
+ """
+ search_results_sql = base_query
+ search_results_params = [
+ *ns_param,
+ *filter_params,
+ op.limit,
+ op.offset,
+ ]
+
+ if op.refresh_ttl:
+ # Wrap entire primary query in a CTE, then perform "update_at"
+ final_sql = f"""
+ WITH search_results AS (
+ {search_results_sql}
+ ),
+ updated AS (
+ UPDATE store s
+ SET expires_at = NOW() + (s.ttl_minutes || ' minutes')::interval
+ FROM search_results sr
+ WHERE s.prefix = sr.prefix
+ AND s.key = sr.key
+ AND s.ttl_minutes IS NOT NULL
+ )
+ SELECT sr.prefix, sr.key, sr.value, sr.created_at, sr.updated_at, sr.score
+ FROM search_results sr
+ """
+ final_params = search_results_params[:] # copy
+ else:
+ final_sql = search_results_sql
+ final_params = search_results_params
+ queries.append((final_sql, final_params))
+
+ return queries, embedding_requests
+
+ def _get_batch_list_namespaces_queries(
+ self,
+ list_ops: Sequence[tuple[int, ListNamespacesOp]],
+ ) -> list[tuple[str, Sequence]]:
+ queries: list[tuple[str, Sequence]] = []
+ for _, op in list_ops:
+ query = r"""
+ SELECT DISTINCT ON (truncated_prefix) truncated_prefix, prefix
+ FROM (
+ SELECT
+ prefix,
+ CASE
+ WHEN %s::integer IS NOT NULL THEN
+ (SELECT STRING_AGG(part, '.' ORDER BY idx)
+ FROM (
+ SELECT part, ROW_NUMBER() OVER () AS idx
+ FROM UNNEST(REGEXP_SPLIT_TO_ARRAY(prefix, '\.')) AS part
+ LIMIT %s::integer
+ ) subquery
+ )
+ ELSE prefix
+ END AS truncated_prefix
+ FROM store
+ """
+ params: list[Any] = [op.max_depth, op.max_depth]
+
+ conditions = []
+ if op.match_conditions:
+ for condition in op.match_conditions:
+ if condition.match_type == "prefix":
+ conditions.append("prefix LIKE %s")
+ params.append(
+ f"{_namespace_to_text(condition.path, handle_wildcards=True)}%"
+ )
+ elif condition.match_type == "suffix":
+ conditions.append("prefix LIKE %s")
+ params.append(
+ f"%{_namespace_to_text(condition.path, handle_wildcards=True)}"
+ )
+ else:
+ logger.warning(
+ f"Unknown match_type in list_namespaces: {condition.match_type}"
+ )
+
+ if conditions:
+ query += " WHERE " + " AND ".join(conditions)
+ query += ") AS subquery "
+
+ query += " ORDER BY truncated_prefix LIMIT %s OFFSET %s"
+ params.extend([op.limit, op.offset])
+ 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]):
+ """Postgres-backed store with optional vector search using pgvector.
+
+ !!! example "Examples"
+ Basic setup and usage:
+ ```python
+ from langgraph.store.postgres import PostgresStore
+ from psycopg import Connection
+
+ conn_string = "postgresql://user:pass@localhost:5432/dbname"
+
+ # Using direct connection
+ with Connection.connect(conn_string) as conn:
+ store = PostgresStore(conn)
+ store.setup() # Run migrations. Done once
+
+ # Store and retrieve data
+ store.put(("users", "123"), "prefs", {"theme": "dark"})
+ item = store.get(("users", "123"), "prefs")
+ ```
+
+ Or using the convenient `from_conn_string` helper:
+
+ ```python
+ from langgraph.store.postgres import PostgresStore
+
+ conn_string = "postgresql://user:pass@localhost:5432/dbname"
+
+ with PostgresStore.from_conn_string(conn_string) as store:
+ 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
+
+ conn_string = "postgresql://user:pass@localhost:5432/dbname"
+
+ with PostgresStore.from_conn_string(
+ conn_string,
+ 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:
+ store.setup() # Do this once to run migrations
+
+ # 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="programming guides", limit=2)
+ ```
+
+ 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.
+ The pgvector extension must be available to use vector search.
+
+ Note:
+ If you provide a TTL configuration, you must explicitly call `start_ttl_sweeper()` to begin
+ the background thread that removes expired items. Call `stop_ttl_sweeper()` to properly
+ clean up resources when you're done with the store.
+
+ """
+
+ __slots__ = (
+ "_deserializer",
+ "pipe",
+ "lock",
+ "supports_pipeline",
+ "index_config",
+ "embeddings",
+ "_ttl_sweeper_thread",
+ "_ttl_stop_event",
+ )
+ supports_ttl: bool = True
+
+ def __init__(
+ self,
+ conn: _pg_internal.Conn,
+ *,
+ pipe: Pipeline | None = None,
+ deserializer: Callable[[bytes | orjson.Fragment], dict[str, Any]] | None = None,
+ index: PostgresIndexConfig | None = None,
+ ttl: TTLConfig | None = None,
+ ) -> None:
+ super().__init__()
+ self._deserializer = deserializer
+ self.conn = 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
+ self.ttl_config = ttl
+ self._ttl_sweeper_thread: threading.Thread | None = None
+ self._ttl_stop_event = threading.Event()
+
+ @classmethod
+ @contextmanager
+ def from_conn_string(
+ cls,
+ conn_string: str,
+ *,
+ pipeline: bool = False,
+ pool_config: PoolConfig | None = None,
+ index: PostgresIndexConfig | None = None,
+ ttl: TTLConfig | None = None,
+ ) -> Iterator[PostgresStore]:
+ """Create a new PostgresStore instance from a connection string.
+
+ Args:
+ conn_string: The Postgres connection info string.
+ pipeline: whether to use Pipeline
+ pool_config: 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: The index configuration for the store.
+ ttl: The TTL configuration for the store.
+
+ 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, index=index, ttl=ttl)
+ 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, index=index, ttl=ttl)
+ else:
+ yield cls(conn, index=index, ttl=ttl)
+
+ def sweep_ttl(self) -> int:
+ """Delete expired store items based on TTL.
+
+ Returns:
+ int: The number of deleted items.
+ """
+ with self._cursor() as cur:
+ cur.execute(
+ """
+ DELETE FROM store
+ WHERE expires_at IS NOT NULL AND expires_at < NOW()
+ """
+ )
+ deleted_count = cur.rowcount
+ return deleted_count
+
+ def start_ttl_sweeper(
+ self, sweep_interval_minutes: int | None = None
+ ) -> concurrent.futures.Future[None]:
+ """Periodically delete expired store items based on TTL.
+
+ Returns:
+ Future that can be waited on or cancelled.
+ """
+ if not self.ttl_config:
+ future: concurrent.futures.Future[None] = concurrent.futures.Future()
+ future.set_result(None)
+ return future
+
+ if self._ttl_sweeper_thread and self._ttl_sweeper_thread.is_alive():
+ logger.info("TTL sweeper thread is already running")
+ # Return a future that can be used to cancel the existing thread
+ future = concurrent.futures.Future()
+ future.add_done_callback(
+ lambda f: self._ttl_stop_event.set() if f.cancelled() else None
+ )
+ return future
+
+ self._ttl_stop_event.clear()
+
+ interval = float(
+ sweep_interval_minutes or self.ttl_config.get("sweep_interval_minutes") or 5
+ )
+ logger.info(f"Starting store TTL sweeper with interval {interval} minutes")
+
+ future = concurrent.futures.Future()
+
+ def _sweep_loop() -> None:
+ try:
+ while not self._ttl_stop_event.is_set():
+ if self._ttl_stop_event.wait(interval * 60):
+ break
+
+ try:
+ expired_items = self.sweep_ttl()
+ if expired_items > 0:
+ logger.info(f"Store swept {expired_items} expired items")
+ except Exception as exc:
+ logger.exception(
+ "Store TTL sweep iteration failed", exc_info=exc
+ )
+ future.set_result(None)
+ except Exception as exc:
+ future.set_exception(exc)
+
+ thread = threading.Thread(target=_sweep_loop, daemon=True, name="ttl-sweeper")
+ self._ttl_sweeper_thread = thread
+ thread.start()
+
+ future.add_done_callback(
+ lambda f: self._ttl_stop_event.set() if f.cancelled() else None
+ )
+ return future
+
+ def stop_ttl_sweeper(self, timeout: float | None = None) -> bool:
+ """Stop the TTL sweeper thread if it's running.
+
+ Args:
+ timeout: Maximum time to wait for the thread to stop, in seconds.
+ If `None`, wait indefinitely.
+
+ Returns:
+ bool: True if the thread was successfully stopped or wasn't running,
+ False if the timeout was reached before the thread stopped.
+ """
+ if not self._ttl_sweeper_thread or not self._ttl_sweeper_thread.is_alive():
+ return True
+
+ logger.info("Stopping TTL sweeper thread")
+ self._ttl_stop_event.set()
+
+ self._ttl_sweeper_thread.join(timeout)
+ success = not self._ttl_sweeper_thread.is_alive()
+
+ if success:
+ self._ttl_sweeper_thread = None
+ logger.info("TTL sweeper thread stopped")
+ else:
+ logger.warning("Timed out waiting for TTL sweeper thread to stop")
+
+ return success
+
+ def __del__(self) -> None:
+ """Ensure the TTL sweeper thread is stopped when the object is garbage collected."""
+ if hasattr(self, "_ttl_stop_event") and hasattr(self, "_ttl_sweeper_thread"):
+ self.stop_ttl_sweeper(timeout=0.1)
+
+ @contextmanager
+ def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor[DictRow]]:
+ """Create a database cursor as a context manager.
+
+ Args:
+ 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.
+ """
+ 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._cursor(pipeline=True) as cur:
+ if GetOp in grouped_ops:
+ self._batch_get_ops(
+ 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:
+ self._batch_list_namespaces_ops(
+ cast(
+ Sequence[tuple[int, ListNamespacesOp]],
+ 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
+
+ def _batch_get_ops(
+ self,
+ get_ops: Sequence[tuple[int, GetOp]],
+ results: list[Result],
+ cur: Cursor[DictRow],
+ ) -> None:
+ for query, params, namespace, items in self._get_batch_GET_ops_queries(get_ops):
+ cur.execute(query, params)
+ rows = cast(list[Row], cur.fetchall())
+ key_to_row = {row["key"]: row for row in rows}
+ for idx, key in items:
+ row = key_to_row.get(key)
+ if row:
+ results[idx] = _row_to_item(
+ namespace, row, loader=self._deserializer
+ )
+ else:
+ results[idx] = None
+
+ def _batch_put_ops(
+ self,
+ put_ops: Sequence[tuple[int, PutOp]],
+ cur: Cursor[DictRow],
+ ) -> None:
+ 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, strict=False
+ )
+ for p in (ns, k, pathname, vector)
+ ],
+ )
+ )
+
+ for query, params in queries:
+ cur.execute(query, params)
+
+ def _batch_search_ops(
+ self,
+ search_ops: Sequence[tuple[int, SearchOp]],
+ results: list[Result],
+ cur: Cursor[DictRow],
+ ) -> None:
+ 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, strict=False
+ ):
+ _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, strict=False):
+ cur.execute(query, params)
+ rows = cast(list[Row], cur.fetchall())
+ results[idx] = [
+ _row_to_search_item(
+ _decode_ns_bytes(row["prefix"]), row, loader=self._deserializer
+ )
+ for row in rows
+ ]
+
+ def _batch_list_namespaces_ops(
+ self,
+ list_ops: Sequence[tuple[int, ListNamespacesOp]],
+ results: list[Result],
+ cur: Cursor[DictRow],
+ ) -> None:
+ for (query, params), (idx, _) in zip(
+ self._get_batch_list_namespaces_queries(list_ops), list_ops, strict=False
+ ):
+ cur.execute(query, params)
+ results[idx] = [_decode_ns_bytes(row["truncated_prefix"]) for row in cur]
+
+ 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.
+
+ 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.
+ """
+
+ def _get_version(cur: Cursor[dict[str, Any]], table: str) -> int:
+ cur.execute(
+ f"""
+ CREATE TABLE IF NOT EXISTS {table} (
+ v INTEGER PRIMARY KEY
+ )
+ """
+ )
+ cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
+ row = cast(dict, cur.fetchone())
+ if row is None:
+ version = -1
+ else:
+ version = row["v"]
+ 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):
+ try:
+ cur.execute(sql)
+ cur.execute("INSERT INTO store_migrations (v) VALUES (%s)", (v,))
+ except Exception as e:
+ logger.error(
+ f"Failed to apply migration {v}.\nSql={sql}\nError={e}"
+ )
+ raise
+
+ if self.index_config:
+ version = _get_version(cur, table="vector_migrations")
+ 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 = {
+ k: v(self) if v is not None and callable(v) else v
+ for k, v in migration.params.items()
+ }
+ if "dims" in params:
+ try:
+ params["dims"] = int(params["dims"])
+ except Exception as e:
+ raise ValueError(
+ f"Invalid dims for vector index: {params['dims']}"
+ ) from e
+ if "vector_type" in params:
+ vt = str(params["vector_type"])
+ if vt not in ("vector", "halfvec"):
+ raise ValueError(
+ f"Invalid vector_type for pgvector: {vt}"
+ )
+ params["vector_type"] = vt
+ if "index_type" in params:
+ it = str(params["index_type"])
+ if it not in ("hnsw", "ivfflat"):
+ raise ValueError(
+ f"Invalid index_type for pgvector: {it}"
+ )
+ params["index_type"] = it
+ sql = sql % params
+ cur.execute(sql)
+ cur.execute("INSERT INTO vector_migrations (v) VALUES (%s)", (v,))
+
+
+class Row(TypedDict):
+ key: str
+ value: Any
+ prefix: str
+ created_at: datetime
+ 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 = 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 _get_index_params(store: Any) -> tuple[str, dict[str, Any]]:
+ """Get a sanitized index type and configuration based on config.
+
+ Only allow known-safe kinds and integer parameters to avoid SQL injection
+ when constructing DDL strings for index creation.
+ """
+ if not store.index_config:
+ return "hnsw", {}
+
+ config = cast(PostgresIndexConfig, store.index_config)
+ raw = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
+
+ kind = str(raw.pop("kind", "hnsw"))
+ if kind not in ("hnsw", "ivfflat", "flat"):
+ raise ValueError(
+ f"Invalid index kind for pgvector: {kind}. Expected 'hnsw', 'ivfflat', or 'flat'."
+ )
+
+ raw.pop("vector_type", None)
+
+ if kind == "hnsw":
+ allowed_keys = {"m", "ef_construction"}
+ else: # ivfflat/flat
+ allowed_keys = {"lists", "nlist"}
+
+ sanitized: dict[str, int] = {}
+ for k, v in list(raw.items()):
+ if k not in allowed_keys:
+ continue
+ key = "lists" if k == "nlist" else k
+ try:
+ ivalue = int(v) # type: ignore[call-overload]
+ except Exception as e:
+ raise ValueError(f"Invalid index parameter value for {k}: {v}") from e
+ if ivalue <= 0:
+ continue
+ sanitized[key] = ivalue
+
+ return kind, sanitized
+
+
+def _namespace_to_text(
+ namespace: tuple[str, ...], handle_wildcards: bool = False
+) -> str:
+ """Convert namespace tuple to text string."""
+ if handle_wildcards:
+ namespace = tuple("%" if val == "*" else val for val in namespace)
+ return ".".join(namespace)
+
+
+def _row_to_item(
+ namespace: tuple[str, ...],
+ row: Row,
+ *,
+ loader: Callable[[bytes | orjson.Fragment], dict[str, Any]] | None = None,
+) -> Item:
+ """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"]
+ 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(
+ namespace: tuple[str, ...],
+ row: Row,
+ *,
+ loader: Callable[[bytes | orjson.Fragment], dict[str, Any]] | None = 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
+ for idx, op in enumerate(ops):
+ grouped_ops[type(op)].append((idx, op))
+ tot += 1
+ return grouped_ops, tot
+
+
+def _json_loads(content: bytes | orjson.Fragment) -> Any:
+ if isinstance(content, orjson.Fragment):
+ if hasattr(content, "buf"):
+ content = content.buf
+ else:
+ if isinstance(content.contents, bytes):
+ content = content.contents
+ else:
+ content = content.contents.encode()
+ return orjson.loads(cast(bytes, content))
+
+
+def _decode_ns_bytes(namespace: str | bytes | list) -> tuple[str, ...]:
+ if isinstance(namespace, list):
+ return tuple(namespace)
+ if isinstance(namespace, bytes):
+ namespace = namespace.decode()[1:]
+ return tuple(namespace.split("."))
+
+
+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
+ # 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")
+
+ # 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":
+ # 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":
+ # 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(
+ index_config: PostgresIndexConfig,
+) -> tuple[Embeddings | None, PostgresIndexConfig]:
+ index_config = index_config.copy()
+ tokenized: list[tuple[str, Literal["$"] | list[str]]] = []
+ tot = 0
+ fields = index_config.get("fields") or ["$"]
+ if isinstance(fields, str):
+ fields = [fields]
+ if not isinstance(fields, list):
+ raise ValueError(f"Text fields must be a list or a string. Got {fields}")
+ for p in 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/langgraph/source/libs/checkpoint-postgres/langgraph/store/postgres/py.typed b/langgraph/source/libs/checkpoint-postgres/langgraph/store/postgres/py.typed
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/checkpoint-postgres/pyproject.toml b/langgraph/source/libs/checkpoint-postgres/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..d776fea8476bb296567b3245766dc6f9c7d79264
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/pyproject.toml
@@ -0,0 +1,86 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "langgraph-checkpoint-postgres"
+version = "3.0.4"
+description = "Library with a Postgres implementation of LangGraph checkpoint saver."
+authors = []
+requires-python = ">=3.10"
+readme = "README.md"
+license = "MIT"
+license-files = ['LICENSE']
+dependencies = [
+ "langgraph-checkpoint>=2.1.2,<5.0.0",
+ "orjson>=3.10.1",
+ "psycopg>=3.2.0",
+ "psycopg-pool>=3.2.0",
+]
+
+[project.urls]
+Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres"
+Twitter = "https://x.com/LangChain"
+Slack = "https://www.langchain.com/join-community"
+Reddit = "https://www.reddit.com/r/LangChain/"
+
+[dependency-groups]
+test = [
+ "pytest",
+ "anyio",
+ "pytest-asyncio",
+ "pytest-mock",
+ "psycopg[binary]",
+ "langgraph-checkpoint",
+ "pytest-watcher",
+]
+lint = [
+ "ruff",
+ "codespell",
+ "mypy",
+]
+dev = [
+ {include-group = "test"},
+ {include-group = "lint"},
+]
+
+[tool.uv]
+default-groups = ['dev']
+
+[tool.uv.sources]
+langgraph-checkpoint = { path = "../checkpoint", editable = true }
+
+[tool.hatch.build.targets.wheel]
+include = ["langgraph"]
+
+[tool.pytest.ini_options]
+addopts = "--strict-markers --strict-config --durations=5 -vv"
+asyncio_mode = "auto"
+
+[tool.ruff]
+lint.select = [
+ "E", # pycodestyle
+ "F", # Pyflakes
+ "UP", # pyupgrade
+ "B", # flake8-bugbear
+ "I", # isort
+ "UP", # pyupgrade
+]
+lint.ignore = ["E501", "B008"]
+target-version = "py310"
+
+[tool.mypy]
+# https://mypy.readthedocs.io/en/stable/config_file.html
+disallow_untyped_defs = "True"
+explicit_package_bases = "True"
+warn_no_return = "False"
+warn_unused_ignores = "True"
+warn_redundant_casts = "True"
+allow_redefinition = "True"
+disable_error_code = "typeddict-item, return-value"
+
+[tool.pytest-watcher]
+now = true
+delay = 0.1
+runner_args = ["--ff", "-x", "-v", "--tb", "short"]
+patterns = ["*.py"]
diff --git a/langgraph/source/libs/checkpoint-postgres/tests/__init__.py b/langgraph/source/libs/checkpoint-postgres/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/checkpoint-postgres/tests/compose-postgres.yml b/langgraph/source/libs/checkpoint-postgres/tests/compose-postgres.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7217844336671dbfd8ba587455fadbdec3715fb7
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/tests/compose-postgres.yml
@@ -0,0 +1,17 @@
+services:
+ postgres-test:
+ 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
+ timeout: 1s
+ retries: 5
+ interval: 60s
+ start_interval: 1s
diff --git a/langgraph/source/libs/checkpoint-postgres/tests/conftest.py b/langgraph/source/libs/checkpoint-postgres/tests/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..b44977ebdd8e967ab589630a593335eb9a06c110
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/tests/conftest.py
@@ -0,0 +1,44 @@
+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_POSTGRES_URI = "postgres://postgres:postgres@localhost:5441/"
+DEFAULT_URI = "postgres://postgres:postgres@localhost:5441/postgres?sslmode=disable"
+
+
+@pytest.fixture(scope="function")
+async def conn() -> AsyncIterator[AsyncConnection[DictRow]]:
+ async with await AsyncConnection.connect(
+ DEFAULT_URI, autocommit=True, prepare_threshold=0, row_factory=dict_row
+ ) as conn:
+ yield conn
+
+
+@pytest.fixture(scope="function", autouse=True)
+async def clear_test_db(conn: AsyncConnection[DictRow]) -> None:
+ """Delete all tables before each test."""
+ try:
+ await conn.execute("DELETE FROM checkpoints")
+ await conn.execute("DELETE FROM checkpoint_blobs")
+ await conn.execute("DELETE FROM checkpoint_writes")
+ await conn.execute("DELETE FROM checkpoint_migrations")
+ except UndefinedTable:
+ pass
+ try:
+ await conn.execute("DELETE FROM store_migrations")
+ 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/langgraph/source/libs/checkpoint-postgres/tests/embed_test_utils.py b/langgraph/source/libs/checkpoint-postgres/tests/embed_test_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..d28cd959f380264e1e651b7782e65c354f33ae4c
--- /dev/null
+++ b/langgraph/source/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/langgraph/source/libs/checkpoint-postgres/tests/test_async.py b/langgraph/source/libs/checkpoint-postgres/tests/test_async.py
new file mode 100644
index 0000000000000000000000000000000000000000..a46163395fffb4e6e8cf4a4c9d93cb68744bbf95
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/tests/test_async.py
@@ -0,0 +1,373 @@
+# type: ignore
+
+from contextlib import asynccontextmanager
+from typing import Any
+from uuid import uuid4
+
+import pytest
+from langchain_core.runnables import RunnableConfig
+from langgraph.checkpoint.base import (
+ EXCLUDED_METADATA_KEYS,
+ Checkpoint,
+ CheckpointMetadata,
+ create_checkpoint,
+ empty_checkpoint,
+)
+from langgraph.checkpoint.serde.types import TASKS
+from psycopg import AsyncConnection
+from psycopg.rows import dict_row
+from psycopg_pool import AsyncConnectionPool
+
+from langgraph.checkpoint.postgres.aio import (
+ AsyncPostgresSaver,
+ AsyncShallowPostgresSaver,
+)
+from tests.conftest import DEFAULT_POSTGRES_URI
+
+
+def _exclude_keys(config: dict[str, Any]) -> dict[str, Any]:
+ return {k: v for k, v in config.items() if k not in EXCLUDED_METADATA_KEYS}
+
+
+@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}")
+
+
+@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:
+ checkpointer = AsyncPostgresSaver(conn)
+ 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 _shallow_saver():
+ """Fixture for shallow 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 = AsyncShallowPostgresSaver(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 == "shallow":
+ async with _shallow_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",
+ "checkpoint_id": "1",
+ "checkpoint_ns": "",
+ }
+ }
+ 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,
+ "score": 1,
+ }
+ metadata_2: CheckpointMetadata = {
+ "source": "loop",
+ "step": 1,
+ "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", "shallow"])
+async def test_combined_metadata(saver_name: str, test_data) -> None:
+ async with _saver(saver_name) as saver:
+ config = {
+ "configurable": {
+ "thread_id": "thread-2",
+ "checkpoint_ns": "",
+ "__super_private_key": "super_private_value",
+ },
+ "metadata": {"run_id": "my_run_id"},
+ }
+ chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
+ metadata: CheckpointMetadata = {
+ "source": "loop",
+ "step": 1,
+ "score": None,
+ }
+ await saver.aput(config, chkpnt, metadata, {})
+ checkpoint = await saver.aget_tuple(config)
+ assert checkpoint.metadata == {
+ **metadata,
+ "run_id": "my_run_id",
+ }
+
+
+@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe", "shallow"])
+async def test_asearch(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,
+ } # 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_1 = [c async for c in saver.alist(None, filter=query_1)]
+ assert len(search_results_1) == 1
+ assert search_results_1[0].metadata == {
+ **_exclude_keys(configs[0]["configurable"]),
+ **metadata[0],
+ }
+
+ 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 == {
+ **_exclude_keys(configs[1]["configurable"]),
+ **metadata[1],
+ }
+
+ search_results_3 = [c async for c in saver.alist(None, filter=query_3)]
+ assert len(search_results_3) == 3
+
+ 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"}
+
+
+@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe", "shallow"])
+async def test_null_chars(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"
+
+
+@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
+async def test_pending_sends_migration(saver_name: str) -> None:
+ async with _saver(saver_name) as saver:
+ config = {
+ "configurable": {
+ "thread_id": "thread-1",
+ "checkpoint_ns": "",
+ }
+ }
+
+ # create the first checkpoint
+ # and put some pending sends
+ checkpoint_0 = empty_checkpoint()
+ config = await saver.aput(config, checkpoint_0, {}, {})
+ await saver.aput_writes(
+ config, [(TASKS, "send-1"), (TASKS, "send-2")], task_id="task-1"
+ )
+ await saver.aput_writes(config, [(TASKS, "send-3")], task_id="task-2")
+
+ # check that fetching checkpoint_0 doesn't attach pending sends
+ # (they should be attached to the next checkpoint)
+ tuple_0 = await saver.aget_tuple(config)
+ assert tuple_0.checkpoint["channel_values"] == {}
+ assert tuple_0.checkpoint["channel_versions"] == {}
+
+ # create the second checkpoint
+ checkpoint_1 = create_checkpoint(checkpoint_0, {}, 1)
+ config = await saver.aput(config, checkpoint_1, {}, {})
+
+ # check that pending sends are attached to checkpoint_1
+ tuple_1 = await saver.aget_tuple(config)
+ assert tuple_1.checkpoint["channel_values"] == {
+ TASKS: ["send-1", "send-2", "send-3"]
+ }
+ assert TASKS in tuple_1.checkpoint["channel_versions"]
+
+ # check that list also applies the migration
+ search_results = [
+ c async for c in saver.alist({"configurable": {"thread_id": "thread-1"}})
+ ]
+ assert len(search_results) == 2
+ assert search_results[-1].checkpoint["channel_values"] == {}
+ assert search_results[-1].checkpoint["channel_versions"] == {}
+ assert search_results[0].checkpoint["channel_values"] == {
+ TASKS: ["send-1", "send-2", "send-3"]
+ }
+ assert TASKS in search_results[0].checkpoint["channel_versions"]
+
+
+@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
+async def test_get_checkpoint_no_channel_values(
+ monkeypatch, saver_name: str, test_data
+) -> None:
+ """Backwards compatibility test that verifies a checkpoint with no channel_values key can be retrieved without throwing an error."""
+ async with _saver(saver_name) as saver:
+ config = {
+ "configurable": {
+ "thread_id": "thread-2",
+ "checkpoint_ns": "",
+ "__super_private_key": "super_private_value",
+ },
+ "metadata": {"run_id": "my_run_id"},
+ }
+ chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
+ await saver.aput(config, chkpnt, {}, {})
+
+ load_checkpoint_tuple = saver._load_checkpoint_tuple
+
+ def patched_load_checkpoint_tuple(value):
+ value["checkpoint"].pop("channel_values", None)
+ return load_checkpoint_tuple(value)
+
+ monkeypatch.setattr(
+ saver, "_load_checkpoint_tuple", patched_load_checkpoint_tuple
+ )
+
+ checkpoint = await saver.aget_tuple(config)
+ assert checkpoint.checkpoint["channel_values"] == {}
diff --git a/langgraph/source/libs/checkpoint-postgres/tests/test_async_store.py b/langgraph/source/libs/checkpoint-postgres/tests/test_async_store.py
new file mode 100644
index 0000000000000000000000000000000000000000..68aee92b7f52729132c10699823b1b918e230ca7
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/tests/test_async_store.py
@@ -0,0 +1,687 @@
+# type: ignore
+from __future__ import annotations
+
+import asyncio
+import itertools
+import uuid
+from collections.abc import AsyncIterator
+from concurrent.futures import ThreadPoolExecutor
+from contextlib import asynccontextmanager
+from typing import Any
+
+import pytest
+from langchain_core.embeddings import Embeddings
+from langgraph.store.base import (
+ GetOp,
+ Item,
+ ListNamespacesOp,
+ PutOp,
+ SearchOp,
+)
+from psycopg import AsyncConnection
+
+from langgraph.store.postgres import AsyncPostgresStore
+from tests.conftest import (
+ DEFAULT_URI,
+ VECTOR_TYPES,
+ CharacterEmbeddings,
+)
+
+TTL_SECONDS = 6
+TTL_MINUTES = TTL_SECONDS / 60
+
+
+@pytest.fixture(scope="function", params=["default", "pipe", "pool"])
+async def store(request) -> AsyncIterator[AsyncPostgresStore]:
+ 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
+ ttl_config = {
+ "default_ttl": TTL_MINUTES,
+ "refresh_on_read": True,
+ "sweep_interval_minutes": TTL_MINUTES / 2,
+ }
+ 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, ttl=ttl_config
+ ) as store:
+ store.MIGRATIONS = [
+ (
+ mig.replace("ttl_minutes INT;", "ttl_minutes FLOAT;")
+ if isinstance(mig, str)
+ else mig
+ )
+ for mig in store.MIGRATIONS
+ ]
+ await store.setup()
+ async with store._cursor() as cur:
+ # drop the migration index
+ await cur.execute("DROP TABLE IF EXISTS store_migrations")
+ await store.setup() # Will fail if migrations aren't idempotent
+
+ if request.param == "pipe":
+ async with AsyncPostgresStore.from_conn_string(
+ conn_string, pipeline=True, ttl=ttl_config
+ ) as store:
+ await store.start_ttl_sweeper()
+ yield store
+ await store.stop_ttl_sweeper()
+ elif request.param == "pool":
+ async with AsyncPostgresStore.from_conn_string(
+ conn_string, pool_config={"min_size": 1, "max_size": 10}, ttl=ttl_config
+ ) as store:
+ await store.start_ttl_sweeper()
+ yield store
+ await store.stop_ttl_sweeper()
+ else: # default
+ async with AsyncPostgresStore.from_conn_string(
+ conn_string, ttl=ttl_config
+ ) as store:
+ await store.start_ttl_sweeper()
+ yield store
+ await store.stop_ttl_sweeper()
+ finally:
+ async with await AsyncConnection.connect(
+ admin_conn_string, autocommit=True
+ ) as conn:
+ await conn.execute(f"DROP DATABASE {database}")
+
+
+async def test_no_running_loop(store: AsyncPostgresStore) -> None:
+ with pytest.raises(asyncio.InvalidStateError):
+ store.put(("foo", "bar"), "baz", {"val": "baz"})
+ with pytest.raises(asyncio.InvalidStateError):
+ store.get(("foo", "bar"), "baz")
+ with pytest.raises(asyncio.InvalidStateError):
+ store.delete(("foo", "bar"), "baz")
+ with pytest.raises(asyncio.InvalidStateError):
+ store.search(("foo", "bar"))
+ with pytest.raises(asyncio.InvalidStateError):
+ store.list_namespaces(prefix=("foo",))
+ with pytest.raises(asyncio.InvalidStateError):
+ store.batch([PutOp(namespace=("foo", "bar"), key="baz", value={"val": "baz"})])
+ with ThreadPoolExecutor(max_workers=1) as executor:
+ future = executor.submit(store.put, ("foo", "bar"), "baz", {"val": "baz"})
+ result = await asyncio.wrap_future(future)
+ assert result is None
+ future = executor.submit(store.get, ("foo", "bar"), "baz")
+ result = await asyncio.wrap_future(future)
+ assert result.value == {"val": "baz"}
+ result = await asyncio.wrap_future(
+ executor.submit(store.list_namespaces, prefix=("foo",))
+ )
+
+
+async def test_large_batches(request: Any, store: AsyncPostgresStore) -> None:
+ N = 100 # less important that we are performant here
+ M = 10
+
+ with ThreadPoolExecutor(max_workers=10) as executor:
+ futures = []
+ for m in range(M):
+ for i in range(N):
+ futures += [
+ executor.submit(
+ store.put,
+ ("test", "foo", "bar", "baz", str(m % 2)),
+ f"key{i}",
+ value={"foo": "bar" + str(i)},
+ ),
+ executor.submit(
+ store.get,
+ ("test", "foo", "bar", "baz", str(m % 2)),
+ f"key{i}",
+ ),
+ executor.submit(
+ store.list_namespaces,
+ prefix=None,
+ max_depth=m + 1,
+ ),
+ executor.submit(
+ store.search,
+ ("test",),
+ ),
+ executor.submit(
+ store.put,
+ ("test", "foo", "bar", "baz", str(m % 2)),
+ f"key{i}",
+ value={"foo": "bar" + str(i)},
+ ),
+ executor.submit(
+ store.put,
+ ("test", "foo", "bar", "baz", str(m % 2)),
+ f"key{i}",
+ None,
+ ),
+ ]
+
+ results = await asyncio.gather(
+ *(asyncio.wrap_future(future) for future in futures)
+ )
+ assert len(results) == M * N * 6
+
+
+async def test_large_batches_async(store: AsyncPostgresStore) -> None:
+ N = 1000
+ M = 10
+ coros = []
+ for m in range(M):
+ for i in range(N):
+ coros.append(
+ store.aput(
+ ("test", "foo", "bar", "baz", str(m % 2)),
+ f"key{i}",
+ value={"foo": "bar" + str(i)},
+ )
+ )
+ coros.append(
+ store.aget(
+ ("test", "foo", "bar", "baz", str(m % 2)),
+ f"key{i}",
+ )
+ )
+ coros.append(
+ store.alist_namespaces(
+ prefix=None,
+ max_depth=m + 1,
+ )
+ )
+ coros.append(
+ store.asearch(
+ ("test",),
+ )
+ )
+ coros.append(
+ store.aput(
+ ("test", "foo", "bar", "baz", str(m % 2)),
+ f"key{i}",
+ value={"foo": "bar" + str(i)},
+ )
+ )
+ coros.append(
+ store.adelete(
+ ("test", "foo", "bar", "baz", str(m % 2)),
+ f"key{i}",
+ )
+ )
+
+ results = await asyncio.gather(*coros)
+ assert len(results) == M * N * 6
+
+
+async def test_abatch_order(store: AsyncPostgresStore) -> None:
+ # Setup test data
+ await store.aput(("test", "foo"), "key1", {"data": "value1"})
+ await store.aput(("test", "bar"), "key2", {"data": "value2"})
+
+ ops = [
+ 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 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 isinstance(results[2], list)
+ assert len(results[2]) == 1
+ assert isinstance(results[3], list)
+ 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", "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", "foo"), key="key1"),
+ ]
+
+ results_reordered = await store.abatch(ops_reordered)
+ assert len(results_reordered) == 5
+ assert isinstance(results_reordered[0], list)
+ 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 ("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"}
+ assert results_reordered[4].key == "key1"
+
+
+async def test_batch_get_ops(store: AsyncPostgresStore) -> None:
+ # Setup test data
+ await store.aput(("test",), "key1", {"data": "value1"})
+ await store.aput(("test",), "key2", {"data": "value2"})
+
+ ops = [
+ GetOp(namespace=("test",), key="key1"),
+ GetOp(namespace=("test",), key="key2"),
+ GetOp(namespace=("test",), key="key3"),
+ ]
+
+ results = await store.abatch(ops)
+
+ assert len(results) == 3
+ assert results[0] is not None
+ assert results[1] is not None
+ assert results[2] is None
+ assert results[0].key == "key1"
+ assert results[1].key == "key2"
+
+
+async def test_batch_put_ops(store: AsyncPostgresStore) -> None:
+ ops = [
+ PutOp(namespace=("test",), key="key1", value={"data": "value1"}),
+ PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
+ PutOp(namespace=("test",), key="key3", value=None),
+ ]
+
+ results = await store.abatch(ops)
+
+ assert len(results) == 3
+ assert all(result is None for result in results)
+
+ # 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:
+ # Setup test data
+ await store.aput(("test", "foo"), "key1", {"data": "value1"})
+ await store.aput(("test", "bar"), "key2", {"data": "value2"})
+
+ ops = [
+ SearchOp(
+ namespace_prefix=("test",), filter={"data": "value1"}, limit=10, offset=0
+ ),
+ SearchOp(namespace_prefix=("test",), filter=None, limit=5, offset=0),
+ ]
+
+ results = await store.abatch(ops)
+
+ assert len(results) == 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:
+ # 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 len(results[0]) == 2
+ assert ("test", "namespace1") in results[0]
+ assert ("test", "namespace2") in results[0]
+
+
+@asynccontextmanager
+async def _create_vector_store(
+ vector_type: str,
+ distance_type: str,
+ fake_embeddings: CharacterEmbeddings,
+ text_fields: list[str] | None = None,
+) -> AsyncIterator[AsyncPostgresStore]:
+ """Create a store with vector search enabled."""
+
+ 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,
+ "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}")
+
+
+@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
+
+
+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
+
+
+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"}),
+ ]
+
+ for key, value in docs:
+ await vector_store.aput(("test",), key, value)
+
+ results = await vector_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
+
+
+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"})
+
+ 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
+
+ await vector_store.aput(("test",), "doc1", {"text": "new text about dogs"})
+
+ 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
+
+ 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
+
+ # 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)
+
+
+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}),
+ ]
+
+ for key, value in docs:
+ await vector_store.aput(("test",), key, value)
+
+ results = await vector_store.asearch(
+ ("test",), query="apple", filter={"color": "red"}
+ )
+ assert len(results) == 2
+ assert results[0].key == "doc1"
+
+ results = await vector_store.asearch(
+ ("test",), query="car", filter={"color": "red"}
+ )
+ assert len(results) == 2
+ assert results[0].key == "doc2"
+
+ results = await vector_store.asearch(
+ ("test",), query="bbbbluuu", filter={"score": {"$gt": 3.2}}
+ )
+ assert len(results) == 3
+ assert results[0].key == "doc4"
+
+ results = await vector_store.asearch(
+ ("test",), query="apple", filter={"score": {"$gte": 4.0}, "color": "green"}
+ )
+ assert len(results) == 1
+ assert results[0].key == "doc3"
+
+
+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}"}
+ )
+
+ results_page1 = await vector_store.asearch(("test",), query="test", limit=2)
+ results_page2 = await vector_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 vector_store.asearch(("test",), query="test", limit=10)
+ assert len(all_results) == 5
+
+
+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"})
+
+ perfect_match = await vector_store.asearch(("test",), query="text test document")
+ perfect_score = perfect_match[0].score
+
+ results = await vector_store.asearch(("test",), query="")
+ assert len(results) == 1
+ assert results[0].score is None
+
+ results = await vector_store.asearch(("test",), query=None)
+ assert len(results) == 1
+ assert results[0].score is None
+
+ long_query = "foo " * 100
+ results = await vector_store.asearch(("test",), query=long_query)
+ assert len(results) == 1
+ assert results[0].score < perfect_score
+
+ special_query = "test!@#$%^&*()"
+ results = await vector_store.asearch(("test",), query=special_query)
+ assert len(results) == 1
+ assert results[0].score < perfect_score
+
+
+@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)
+
+ # 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)
+
+ 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)
+
+ # 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
+
+
+async def test_store_ttl(store):
+ # Assumes a TTL of 1 minute = 60 seconds
+ ns = ("foo",)
+ await store.start_ttl_sweeper()
+ await store.aput(
+ ns,
+ key="item1",
+ value={"foo": "bar"},
+ ttl=TTL_MINUTES, # type: ignore
+ )
+ await asyncio.sleep(TTL_SECONDS - 2)
+ res = await store.aget(ns, key="item1", refresh_ttl=True)
+ assert res is not None
+ await asyncio.sleep(TTL_SECONDS - 2)
+ results = await store.asearch(ns, query="foo", refresh_ttl=True)
+ assert len(results) == 1
+ await asyncio.sleep(TTL_SECONDS - 2)
+ res = await store.aget(ns, key="item1", refresh_ttl=False)
+ assert res is not None
+ await asyncio.sleep(TTL_SECONDS - 1)
+ # Now has been (TTL_SECONDS-2)*2 > TTL_SECONDS + TTL_SECONDS/2
+ results = await store.asearch(ns, query="bar", refresh_ttl=False)
+ assert len(results) == 0
diff --git a/langgraph/source/libs/checkpoint-postgres/tests/test_store.py b/langgraph/source/libs/checkpoint-postgres/tests/test_store.py
new file mode 100644
index 0000000000000000000000000000000000000000..ac8d165583bdd216084d663b2a0b0ca4ee89eaa9
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/tests/test_store.py
@@ -0,0 +1,901 @@
+# type: ignore
+from __future__ import annotations
+
+import re
+import time
+from contextlib import contextmanager
+from typing import Any
+from uuid import uuid4
+
+import pytest
+from langchain_core.embeddings import Embeddings
+from langgraph.store.base import (
+ GetOp,
+ Item,
+ ListNamespacesOp,
+ MatchCondition,
+ PutOp,
+ SearchOp,
+)
+from psycopg import Connection
+
+from langgraph.store.postgres import PostgresStore
+from tests.conftest import (
+ DEFAULT_URI,
+ VECTOR_TYPES,
+ CharacterEmbeddings,
+)
+
+TTL_SECONDS = 6
+TTL_MINUTES = TTL_SECONDS / 60
+
+
+@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]:
+ _, query_params = uri_parts[-1].split("?", 1)
+ query_params = "?" + query_params
+
+ conn_string = f"{uri_base}/{database}{query_params}"
+ admin_conn_string = DEFAULT_URI
+ ttl_config = {
+ "default_ttl": TTL_MINUTES,
+ "refresh_on_read": True,
+ "sweep_interval_minutes": TTL_MINUTES / 2,
+ }
+ with Connection.connect(admin_conn_string, autocommit=True) as conn:
+ conn.execute(f"CREATE DATABASE {database}")
+ try:
+ with PostgresStore.from_conn_string(conn_string, ttl=ttl_config) as store:
+ store.MIGRATIONS = [
+ (
+ mig.replace("ttl_minutes INT;", "ttl_minutes FLOAT;")
+ if isinstance(mig, str)
+ else mig
+ )
+ for mig in store.MIGRATIONS
+ ]
+ store.setup()
+
+ if request.param == "pipe":
+ with PostgresStore.from_conn_string(
+ conn_string,
+ pipeline=True,
+ ttl=ttl_config,
+ ) as store:
+ store.start_ttl_sweeper()
+ yield store
+
+ store.stop_ttl_sweeper()
+ elif request.param == "pool":
+ with PostgresStore.from_conn_string(
+ conn_string,
+ pool_config={"min_size": 1, "max_size": 10},
+ ttl=ttl_config,
+ ) as store:
+ store.start_ttl_sweeper()
+ yield store
+
+ store.stop_ttl_sweeper()
+ else: # default
+ with PostgresStore.from_conn_string(conn_string, ttl=ttl_config) as store:
+ store.start_ttl_sweeper()
+ yield store
+
+ store.stop_ttl_sweeper()
+ finally:
+ with Connection.connect(admin_conn_string, autocommit=True) as conn:
+ conn.execute(f"DROP DATABASE {database}")
+
+
+def test_batch_order(store: PostgresStore) -> None:
+ # Setup test data
+ store.put(("test", "foo"), "key1", {"data": "value1"})
+ store.put(("test", "bar"), "key2", {"data": "value2"})
+
+ ops = [
+ 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 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 # Put operation returns None
+ assert isinstance(results[2], list)
+ assert len(results[2]) == 1
+ assert isinstance(results[3], list)
+ 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", "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", "foo"), key="key1"),
+ ]
+
+ results_reordered = store.batch(ops_reordered)
+ assert len(results_reordered) == 5
+ assert isinstance(results_reordered[0], list)
+ 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 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:
+ # 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"), # Non-existent key
+ ]
+
+ results = store.batch(ops)
+
+ assert len(results) == 3
+ assert results[0] is not None
+ assert results[1] is not None
+ assert results[2] is None
+ assert results[0].key == "key1"
+ assert results[1].key == "key2"
+
+
+def test_batch_put_ops(store: PostgresStore) -> None:
+ ops = [
+ PutOp(namespace=("test",), key="key1", value={"data": "value1"}),
+ PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
+ 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)
+
+ # 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:
+ # 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={"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
+
+ # 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:
+ # 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),
+ 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
+
+ # 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])
+
+
+def test_basic_store_ops(store) -> None:
+ namespace = ("test", "documents")
+ item_id = "doc1"
+ item_value = {"title": "Test Document", "content": "Hello, World!"}
+
+ store.put(namespace, item_id, item_value)
+ item = store.get(namespace, item_id)
+
+ assert item
+ assert item.namespace == namespace
+ assert item.key == item_id
+ assert item.value == item_value
+
+ # 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
+
+ # Test delete
+ store.delete(namespace, item_id)
+ deleted_item = store.get(namespace, item_id)
+ assert deleted_item is None
+
+
+def test_list_namespaces(store) -> None:
+ # Create test data with various namespaces
+ test_namespaces = [
+ ("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"})
+
+ # Test listing with various filters
+ all_namespaces = store.list_namespaces()
+ assert len(all_namespaces) == len(test_namespaces)
+
+ # 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)
+
+ # 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)
+
+ # Test max depth
+ depth_2_namespaces = store.list_namespaces(max_depth=2)
+ assert all(len(ns) <= 2 for ns in depth_2_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(store) -> None:
+ # 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, key, value in test_data:
+ store.put(namespace, key, value)
+
+ # Test basic search
+ all_items = store.search(["test"])
+ assert len(all_items) == 3
+
+ # 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)
+
+ # 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)
+
+ # Test pagination
+ paginated_items = store.search(["test"], limit=2)
+ assert len(paginated_items) == 2
+
+ offset_items = store.search(["test"], offset=2)
+ assert len(offset_items) == 1
+
+ # 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: list[str] | None = None,
+ enable_ttl: bool = True,
+) -> 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,
+ "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,
+ ttl={"default_ttl": 2, "refresh_on_read": True} if enable_ttl else None,
+ ) as store:
+ store.setup()
+ with store._cursor() as cur:
+ # drop the migration index
+ cur.execute("DROP TABLE IF EXISTS store_migrations")
+ store.setup() # Will fail if migrations aren't idempotent
+ yield store
+ finally:
+ with Connection.connect(admin_conn_string, autocommit=True) as conn:
+ conn.execute(f"DROP DATABASE {database}")
+
+
+_vector_params = [
+ (vector_type, distance_type, True)
+ for vector_type in VECTOR_TYPES
+ for distance_type in (
+ ["hamming"] if vector_type == "bit" else ["l2", "inner_product", "cosine"]
+ )
+]
+_vector_params += [(*_vector_params[-1][:2], False)]
+
+
+@pytest.fixture(
+ scope="function",
+ params=_vector_params,
+ 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, enable_ttl = request.param
+ with _create_vector_store(
+ vector_type, distance_type, fake_embeddings, enable_ttl=enable_ttl
+ ) 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)
+
+
+@pytest.mark.parametrize("refresh_ttl", [True, False])
+def test_vector_search_with_filters(
+ vector_store: PostgresStore, refresh_ttl: bool
+) -> 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"}, refresh_ttl=refresh_ttl
+ )
+ assert len(results) == 2
+ assert results[0].key == "doc1"
+
+ results = vector_store.search(
+ ("test",), query="car", filter={"color": "red"}, refresh_ttl=refresh_ttl
+ )
+ assert len(results) == 2
+ assert results[0].key == "doc2"
+
+ results = vector_store.search(
+ ("test",),
+ query="bbbbluuu",
+ filter={"score": {"$gt": 3.2}},
+ refresh_ttl=refresh_ttl,
+ )
+ 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), f"{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
+
+
+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, strict=False))
+ 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, strict=False))
+ 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, strict=False)) ** 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)
+
+
+def test_nonnull_migrations() -> None:
+ _leading_comment_remover = re.compile(r"^/\*.*?\*/")
+ for migration in PostgresStore.MIGRATIONS:
+ statement = _leading_comment_remover.sub("", migration).split()[0]
+ assert statement.strip()
+
+
+def test_store_ttl(store):
+ # Assumes a TTL of 1 minute = 60 seconds
+ ns = ("foo",)
+ store.put(
+ ns,
+ key="item1",
+ value={"foo": "bar"},
+ ttl=TTL_MINUTES, # type: ignore
+ )
+ time.sleep(TTL_SECONDS - 2)
+ res = store.get(ns, key="item1", refresh_ttl=True)
+ assert res is not None
+ time.sleep(TTL_SECONDS - 2)
+ results = store.search(ns, query="foo", refresh_ttl=True)
+ assert len(results) == 1
+ time.sleep(TTL_SECONDS - 2)
+ res = store.get(ns, key="item1", refresh_ttl=False)
+ assert res is not None
+ time.sleep(TTL_SECONDS - 1)
+ # Now has been (TTL_SECONDS-2)*2 > TTL_SECONDS + TTL_SECONDS/2
+ res = store.search(ns, query="bar", refresh_ttl=False)
+ assert len(res) == 0
+
+
+@pytest.mark.parametrize(
+ "vector_type,distance_type",
+ [
+ ("vector", "cosine"),
+ ("vector", "inner_product"),
+ ("halfvec", "cosine"),
+ ("halfvec", "inner_product"),
+ ],
+)
+def test_non_ascii(
+ request: Any,
+ fake_embeddings: CharacterEmbeddings,
+ vector_type: str,
+ distance_type: str,
+) -> None:
+ """Test support for non-ascii characters"""
+ with _create_vector_store(vector_type, distance_type, fake_embeddings) as store:
+ store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
+ store.put(
+ ("user_123", "memories"), "2", {"text": "これは日本語です"}
+ ) # Japanese
+ store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
+ store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
+ store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
+
+ result1 = store.search(("user_123", "memories"), query="这是中文")
+ result2 = store.search(("user_123", "memories"), query="これは日本語です")
+ result3 = store.search(("user_123", "memories"), query="이건 한국어야")
+ result4 = store.search(("user_123", "memories"), query="Это русский")
+ result5 = store.search(("user_123", "memories"), query="यह रूसी है")
+
+ assert result1[0].key == "1"
+ assert result2[0].key == "2"
+ assert result3[0].key == "3"
+ assert result4[0].key == "4"
+ assert result5[0].key == "5"
diff --git a/langgraph/source/libs/checkpoint-postgres/tests/test_sync.py b/langgraph/source/libs/checkpoint-postgres/tests/test_sync.py
new file mode 100644
index 0000000000000000000000000000000000000000..63b93715f23f36e529545057dcc3f0cb2b2033c9
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/tests/test_sync.py
@@ -0,0 +1,360 @@
+# type: ignore
+
+import re
+from contextlib import contextmanager
+from typing import Any
+from uuid import uuid4
+
+import pytest
+from langchain_core.runnables import RunnableConfig
+from langgraph.checkpoint.base import (
+ EXCLUDED_METADATA_KEYS,
+ Checkpoint,
+ CheckpointMetadata,
+ create_checkpoint,
+ empty_checkpoint,
+)
+from langgraph.checkpoint.serde.types import TASKS
+from psycopg import Connection
+from psycopg.rows import dict_row
+from psycopg_pool import ConnectionPool
+
+from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
+from tests.conftest import DEFAULT_POSTGRES_URI
+
+
+def _exclude_keys(config: dict[str, Any]) -> dict[str, Any]:
+ return {k: v for k, v in config.items() if k not in EXCLUDED_METADATA_KEYS}
+
+
+@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}")
+
+
+@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:
+ checkpointer = PostgresSaver(conn)
+ 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 _shallow_saver():
+ """Fixture for regular connection mode testing with a shallow checkpointer."""
+ 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 = ShallowPostgresSaver(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 == "shallow":
+ with _shallow_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",
+ "checkpoint_id": "1",
+ "checkpoint_ns": "",
+ }
+ }
+ 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,
+ "score": 1,
+ }
+ metadata_2: CheckpointMetadata = {
+ "source": "loop",
+ "step": 1,
+ "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", "shallow"])
+def test_combined_metadata(saver_name: str, test_data) -> None:
+ with _saver(saver_name) as saver:
+ config = {
+ "configurable": {
+ "thread_id": "thread-2",
+ "checkpoint_ns": "",
+ "__super_private_key": "super_private_value",
+ },
+ "metadata": {"run_id": "my_run_id"},
+ }
+ chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
+ metadata: CheckpointMetadata = {
+ "source": "loop",
+ "step": 1,
+ "score": None,
+ }
+ saver.put(config, chkpnt, metadata, {})
+ checkpoint = saver.get_tuple(config)
+ assert checkpoint.metadata == {
+ **metadata,
+ "run_id": "my_run_id",
+ }
+
+
+@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe", "shallow"])
+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,
+ } # 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_1 = list(saver.list(None, filter=query_1))
+ assert len(search_results_1) == 1
+ assert search_results_1[0].metadata == {
+ **_exclude_keys(configs[0]["configurable"]),
+ **metadata[0],
+ }
+
+ search_results_2 = list(saver.list(None, filter=query_2))
+ assert len(search_results_2) == 1
+ assert search_results_2[0].metadata == {
+ **_exclude_keys(configs[1]["configurable"]),
+ **metadata[1],
+ }
+
+ search_results_3 = list(saver.list(None, filter=query_3))
+ assert len(search_results_3) == 3
+
+ 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"}
+
+
+@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe", "shallow"])
+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"
+ )
+
+
+def test_nonnull_migrations() -> None:
+ _leading_comment_remover = re.compile(r"^/\*.*?\*/")
+ for migration in PostgresSaver.MIGRATIONS:
+ statement = _leading_comment_remover.sub("", migration).split()[0]
+ assert statement.strip()
+
+
+@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
+def test_pending_sends_migration(saver_name: str) -> None:
+ with _saver(saver_name) as saver:
+ config = {
+ "configurable": {
+ "thread_id": "thread-1",
+ "checkpoint_ns": "",
+ }
+ }
+
+ # create the first checkpoint
+ # and put some pending sends
+ checkpoint_0 = empty_checkpoint()
+ config = saver.put(config, checkpoint_0, {}, {})
+ saver.put_writes(
+ config, [(TASKS, "send-1"), (TASKS, "send-2")], task_id="task-1"
+ )
+ saver.put_writes(config, [(TASKS, "send-3")], task_id="task-2")
+
+ # check that fetching checkpoint_0 doesn't attach pending sends
+ # (they should be attached to the next checkpoint)
+ tuple_0 = saver.get_tuple(config)
+ assert tuple_0.checkpoint["channel_values"] == {}
+ assert tuple_0.checkpoint["channel_versions"] == {}
+
+ # create the second checkpoint
+ checkpoint_1 = create_checkpoint(checkpoint_0, {}, 1)
+ config = saver.put(config, checkpoint_1, {}, {})
+
+ # check that pending sends are attached to checkpoint_1
+ checkpoint_1 = saver.get_tuple(config)
+ assert checkpoint_1.checkpoint["channel_values"] == {
+ TASKS: ["send-1", "send-2", "send-3"]
+ }
+ assert TASKS in checkpoint_1.checkpoint["channel_versions"]
+
+ # check that list also applies the migration
+ search_results = [
+ c for c in saver.list({"configurable": {"thread_id": "thread-1"}})
+ ]
+ assert len(search_results) == 2
+ assert search_results[-1].checkpoint["channel_values"] == {}
+ assert search_results[-1].checkpoint["channel_versions"] == {}
+ assert search_results[0].checkpoint["channel_values"] == {
+ TASKS: ["send-1", "send-2", "send-3"]
+ }
+ assert TASKS in search_results[0].checkpoint["channel_versions"]
+
+
+@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
+def test_get_checkpoint_no_channel_values(
+ monkeypatch, saver_name: str, test_data
+) -> None:
+ """Backwards compatibility test that verifies a checkpoint with no channel_values key can be retrieved without throwing an error."""
+ with _saver(saver_name) as saver:
+ config = {
+ "configurable": {
+ "thread_id": "thread-2",
+ "checkpoint_ns": "",
+ "__super_private_key": "super_private_value",
+ },
+ }
+ chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
+ saver.put(config, chkpnt, {}, {})
+
+ load_checkpoint_tuple = saver._load_checkpoint_tuple
+
+ def patched_load_checkpoint_tuple(value):
+ value["checkpoint"].pop("channel_values", None)
+ return load_checkpoint_tuple(value)
+
+ monkeypatch.setattr(
+ saver, "_load_checkpoint_tuple", patched_load_checkpoint_tuple
+ )
+
+ checkpoint = saver.get_tuple(config)
+ assert checkpoint.checkpoint["channel_values"] == {}
diff --git a/langgraph/source/libs/checkpoint-postgres/uv.lock b/langgraph/source/libs/checkpoint-postgres/uv.lock
new file mode 100644
index 0000000000000000000000000000000000000000..6f17c71bf74120684f2ebe15e0aaa643547c069a
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-postgres/uv.lock
@@ -0,0 +1,1351 @@
+version = 1
+revision = 3
+requires-python = ">=3.10"
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.12.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
+]
+
+[[package]]
+name = "backports-asyncio-runner"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.1.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
+ { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
+ { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
+ { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
+ { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
+ { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
+ { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
+ { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
+ { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
+ { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
+ { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
+ { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
+ { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
+ { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
+ { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
+ { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
+ { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
+ { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
+ { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
+]
+
+[[package]]
+name = "codespell"
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740, upload-time = "2025-01-28T18:52:39.411Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501, upload-time = "2025-01-28T18:52:37.057Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "jsonpatch"
+version = "1.33"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonpointer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" },
+]
+
+[[package]]
+name = "jsonpointer"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" },
+]
+
+[[package]]
+name = "langchain-core"
+version = "1.2.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonpatch" },
+ { name = "langsmith" },
+ { name = "packaging" },
+ { name = "pydantic" },
+ { name = "pyyaml" },
+ { name = "tenacity" },
+ { name = "typing-extensions" },
+ { name = "uuid-utils" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a2/0e/664d8d81b3493e09cbab72448d2f9d693d1fa5aa2bcc488602203a9b6da0/langchain_core-1.2.7.tar.gz", hash = "sha256:e1460639f96c352b4a41c375f25aeb8d16ffc1769499fb1c20503aad59305ced", size = 837039, upload-time = "2026-01-09T17:44:25.505Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6e/6f/34a9fba14d191a67f7e2ee3dbce3e9b86d2fa7310e2c7f2c713583481bd2/langchain_core-1.2.7-py3-none-any.whl", hash = "sha256:452f4fef7a3d883357b22600788d37e3d8854ef29da345b7ac7099f33c31828b", size = 490232, upload-time = "2026-01-09T17:44:24.236Z" },
+]
+
+[[package]]
+name = "langgraph-checkpoint"
+version = "4.0.0"
+source = { editable = "../checkpoint" }
+dependencies = [
+ { name = "langchain-core" },
+ { name = "ormsgpack" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "langchain-core", specifier = ">=0.2.38" },
+ { name = "ormsgpack", specifier = ">=1.12.0" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "codespell" },
+ { name = "dataclasses-json" },
+ { name = "mypy" },
+ { name = "numpy" },
+ { name = "pandas" },
+ { name = "pandas-stubs", specifier = ">=2.2.2.240807" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watcher" },
+ { name = "redis" },
+ { name = "ruff" },
+]
+lint = [
+ { name = "codespell" },
+ { name = "mypy" },
+ { name = "ruff" },
+]
+test = [
+ { name = "dataclasses-json" },
+ { name = "numpy" },
+ { name = "pandas" },
+ { name = "pandas-stubs", specifier = ">=2.2.2.240807" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watcher" },
+ { name = "redis" },
+]
+
+[[package]]
+name = "langgraph-checkpoint-postgres"
+version = "3.0.4"
+source = { editable = "." }
+dependencies = [
+ { name = "langgraph-checkpoint" },
+ { name = "orjson" },
+ { name = "psycopg" },
+ { name = "psycopg-pool" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "anyio" },
+ { name = "codespell" },
+ { name = "langgraph-checkpoint" },
+ { name = "mypy" },
+ { name = "psycopg", extra = ["binary"] },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watcher" },
+ { name = "ruff" },
+]
+lint = [
+ { name = "codespell" },
+ { name = "mypy" },
+ { name = "ruff" },
+]
+test = [
+ { name = "anyio" },
+ { name = "langgraph-checkpoint" },
+ { name = "psycopg", extra = ["binary"] },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watcher" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "langgraph-checkpoint", editable = "../checkpoint" },
+ { name = "orjson", specifier = ">=3.10.1" },
+ { name = "psycopg", specifier = ">=3.2.0" },
+ { name = "psycopg-pool", specifier = ">=3.2.0" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "anyio" },
+ { name = "codespell" },
+ { name = "langgraph-checkpoint", editable = "../checkpoint" },
+ { name = "mypy" },
+ { name = "psycopg", extras = ["binary"] },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watcher" },
+ { name = "ruff" },
+]
+lint = [
+ { name = "codespell" },
+ { name = "mypy" },
+ { name = "ruff" },
+]
+test = [
+ { name = "anyio" },
+ { name = "langgraph-checkpoint", editable = "../checkpoint" },
+ { name = "psycopg", extras = ["binary"] },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watcher" },
+]
+
+[[package]]
+name = "langsmith"
+version = "0.6.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "httpx" },
+ { name = "orjson", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "packaging" },
+ { name = "pydantic" },
+ { name = "requests" },
+ { name = "requests-toolbelt" },
+ { name = "uuid-utils" },
+ { name = "zstandard" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e7/85/9c7933052a997da1b85bc5c774f3865e9b1da1c8d71541ea133178b13229/langsmith-0.6.4.tar.gz", hash = "sha256:36f7223a01c218079fbb17da5e536ebbaf5c1468c028abe070aa3ae59bc99ec8", size = 919964, upload-time = "2026-01-15T20:02:28.873Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/66/0f/09a6637a7ba777eb307b7c80852d9ee26438e2bdafbad6fcc849ff9d9192/langsmith-0.6.4-py3-none-any.whl", hash = "sha256:ac4835860160be371042c7adbba3cb267bcf8d96a5ea976c33a8a4acad6c5486", size = 283503, upload-time = "2026-01-15T20:02:26.662Z" },
+]
+
+[[package]]
+name = "librt"
+version = "0.7.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" },
+ { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" },
+ { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" },
+ { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" },
+ { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" },
+ { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" },
+ { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" },
+ { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" },
+ { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" },
+ { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" },
+ { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" },
+ { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" },
+ { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" },
+ { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" },
+ { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" },
+ { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" },
+ { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" },
+ { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" },
+ { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" },
+ { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" },
+ { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" },
+ { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" },
+ { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" },
+ { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" },
+]
+
+[[package]]
+name = "mypy"
+version = "1.19.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "mypy-extensions" },
+ { name = "pathspec" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" },
+ { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" },
+ { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" },
+ { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
+ { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
+ { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" },
+ { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
+ { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
+ { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
+ { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
+ { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" },
+ { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" },
+ { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "orjson"
+version = "3.11.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/19/b22cf9dad4db20c8737041046054cbd4f38bb5a2d0e4bb60487832ce3d76/orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1", size = 245719, upload-time = "2025-12-06T15:53:43.877Z" },
+ { url = "https://files.pythonhosted.org/packages/03/2e/b136dd6bf30ef5143fbe76a4c142828b55ccc618be490201e9073ad954a1/orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870", size = 132467, upload-time = "2025-12-06T15:53:45.379Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/fc/ae99bfc1e1887d20a0268f0e2686eb5b13d0ea7bbe01de2b566febcd2130/orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09", size = 130702, upload-time = "2025-12-06T15:53:46.659Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/43/ef7912144097765997170aca59249725c3ab8ef6079f93f9d708dd058df5/orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd", size = 135907, upload-time = "2025-12-06T15:53:48.487Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/da/24d50e2d7f4092ddd4d784e37a3fa41f22ce8ed97abc9edd222901a96e74/orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac", size = 139935, upload-time = "2025-12-06T15:53:49.88Z" },
+ { url = "https://files.pythonhosted.org/packages/02/4a/b4cb6fcbfff5b95a3a019a8648255a0fac9b221fbf6b6e72be8df2361feb/orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e", size = 137541, upload-time = "2025-12-06T15:53:51.226Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/99/a11bd129f18c2377c27b2846a9d9be04acec981f770d711ba0aaea563984/orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f", size = 139031, upload-time = "2025-12-06T15:53:52.309Z" },
+ { url = "https://files.pythonhosted.org/packages/64/29/d7b77d7911574733a036bb3e8ad7053ceb2b7d6ea42208b9dbc55b23b9ed/orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18", size = 141622, upload-time = "2025-12-06T15:53:53.606Z" },
+ { url = "https://files.pythonhosted.org/packages/93/41/332db96c1de76b2feda4f453e91c27202cd092835936ce2b70828212f726/orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a", size = 413800, upload-time = "2025-12-06T15:53:54.866Z" },
+ { url = "https://files.pythonhosted.org/packages/76/e1/5a0d148dd1f89ad2f9651df67835b209ab7fcb1118658cf353425d7563e9/orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7", size = 151198, upload-time = "2025-12-06T15:53:56.383Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/96/8db67430d317a01ae5cf7971914f6775affdcfe99f5bff9ef3da32492ecc/orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401", size = 141984, upload-time = "2025-12-06T15:53:57.746Z" },
+ { url = "https://files.pythonhosted.org/packages/71/49/40d21e1aa1ac569e521069228bb29c9b5a350344ccf922a0227d93c2ed44/orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8", size = 135272, upload-time = "2025-12-06T15:53:59.769Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/7e/d0e31e78be0c100e08be64f48d2850b23bcb4d4c70d114f4e43b39f6895a/orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167", size = 133360, upload-time = "2025-12-06T15:54:01.25Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" },
+ { url = "https://files.pythonhosted.org/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c6/0a8caff96f4503f4f7dd44e40e90f4d14acf80d3b7a97cb88747bb712d3e/orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7", size = 130516, upload-time = "2025-12-06T15:54:06.274Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/63/43d4dc9bd9954bff7052f700fdb501067f6fb134a003ddcea2a0bb3854ed/orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd", size = 135695, upload-time = "2025-12-06T15:54:07.702Z" },
+ { url = "https://files.pythonhosted.org/packages/87/6f/27e2e76d110919cb7fcb72b26166ee676480a701bcf8fc53ac5d0edce32f/orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9", size = 139664, upload-time = "2025-12-06T15:54:08.828Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/f8/5966153a5f1be49b5fbb8ca619a529fde7bc71aa0a376f2bb83fed248bcd/orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef", size = 137289, upload-time = "2025-12-06T15:54:09.898Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/34/8acb12ff0299385c8bbcbb19fbe40030f23f15a6de57a9c587ebf71483fb/orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9", size = 138784, upload-time = "2025-12-06T15:54:11.022Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/27/910421ea6e34a527f73d8f4ee7bdffa48357ff79c7b8d6eb6f7b82dd1176/orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125", size = 141322, upload-time = "2025-12-06T15:54:12.427Z" },
+ { url = "https://files.pythonhosted.org/packages/87/a3/4b703edd1a05555d4bb1753d6ce44e1a05b7a6d7c164d5b332c795c63d70/orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814", size = 413612, upload-time = "2025-12-06T15:54:13.858Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/36/034177f11d7eeea16d3d2c42a1883b0373978e08bc9dad387f5074c786d8/orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5", size = 150993, upload-time = "2025-12-06T15:54:15.189Z" },
+ { url = "https://files.pythonhosted.org/packages/44/2f/ea8b24ee046a50a7d141c0227c4496b1180b215e728e3b640684f0ea448d/orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880", size = 141774, upload-time = "2025-12-06T15:54:16.451Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/12/cc440554bf8200eb23348a5744a575a342497b65261cd65ef3b28332510a/orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d", size = 135109, upload-time = "2025-12-06T15:54:17.73Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/83/e0c5aa06ba73a6760134b169f11fb970caa1525fa4461f94d76e692299d9/orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1", size = 133193, upload-time = "2025-12-06T15:54:19.426Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/35/5b77eaebc60d735e832c5b1a20b155667645d123f09d471db0a78280fb49/orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c", size = 126830, upload-time = "2025-12-06T15:54:20.836Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" },
+ { url = "https://files.pythonhosted.org/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" },
+ { url = "https://files.pythonhosted.org/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" },
+ { url = "https://files.pythonhosted.org/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" },
+ { url = "https://files.pythonhosted.org/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" },
+ { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" },
+ { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" },
+ { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" },
+ { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" },
+ { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" },
+ { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" },
+ { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" },
+ { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" },
+ { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" },
+ { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" },
+ { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" },
+ { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" },
+ { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" },
+ { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" },
+]
+
+[[package]]
+name = "ormsgpack"
+version = "1.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/96/34c40d621996c2f377a18decbd3c59f031dde73c3ba47d1e1e8f29a05aaa/ormsgpack-1.12.1.tar.gz", hash = "sha256:a3877fde1e4f27a39f92681a0aab6385af3a41d0c25375d33590ae20410ea2ac", size = 39476, upload-time = "2025-12-14T07:57:43.248Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/da/caf25cc54d6870089a0b5614c4c5914dd3fae45f9f7f84a32445ad0612e3/ormsgpack-1.12.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:62e3614cab63fa5aa42f5f0ca3cd12899f0bfc5eb8a5a0ebab09d571c89d427d", size = 376182, upload-time = "2025-12-14T07:56:46.094Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/02/ccc9170c6bee86f428707f15b5ad68d42c71d43856e1b8e37cdfea50af5b/ormsgpack-1.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86d9fbf85c05c69c33c229d2eba7c8c3500a56596cd8348131c918acd040d6af", size = 202339, upload-time = "2025-12-14T07:56:47.609Z" },
+ { url = "https://files.pythonhosted.org/packages/86/c7/10309a5a6421adaedab710a72470143d664bb0a043cc095c1311878325a0/ormsgpack-1.12.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8d246e66f09d8e0f96e770829149ee83206e90ed12f5987998bb7be84aec99fe", size = 210720, upload-time = "2025-12-14T07:56:48.66Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/b4/92a0f7a00c5f0c71b51dc3112e53b1ca937b9891a08979d06524db11b799/ormsgpack-1.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfc2c830a1ed2d00de713d08c9e62efa699e8fd29beafa626aaebe466f583ebb", size = 211264, upload-time = "2025-12-14T07:56:49.976Z" },
+ { url = "https://files.pythonhosted.org/packages/33/fa/5cce85c8e58fcaa048c75fbbe37816a1b3fb58ba4289a7dedc4f4ed9ce82/ormsgpack-1.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc892757d8f9eea5208268a527cf93c98409802f6a9f7c8d71a7b8f9ba5cb944", size = 386076, upload-time = "2025-12-14T07:56:51.74Z" },
+ { url = "https://files.pythonhosted.org/packages/88/d0/f18d258c733eb22eadad748659f7984d0b6a851fb3deefcb33f50e9a947a/ormsgpack-1.12.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0de1dbcf11ea739ac4a882b43d5c2055e6d99ce64e8d6502e25d6d881700c017", size = 479570, upload-time = "2025-12-14T07:56:52.912Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/3a/b362dff090f4740090fe51d512f24b1e320d1f96497ebf9248e2a04ac88f/ormsgpack-1.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d5065dfb9ec4db93241c60847624d9aeef4ccb449c26a018c216b55c69be83c0", size = 387859, upload-time = "2025-12-14T07:56:53.968Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/8a/d948965598b2b7872800076da5c02573aa72f716be57a3d4fe60490b2a2a/ormsgpack-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d17103c4726181d7000c61b751c881f1b6f401d146df12da028fc730227df19", size = 115906, upload-time = "2025-12-14T07:56:55.068Z" },
+ { url = "https://files.pythonhosted.org/packages/57/e2/f5b89365c8dc8025c27d31316038f1c103758ddbf87dc0fa8e3f78f66907/ormsgpack-1.12.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4038f59ae0e19dac5e5d9aae4ec17ff84a79e046342ee73ccdecf3547ecf0d34", size = 376180, upload-time = "2025-12-14T07:56:56.521Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/87/3f694e06f5e32c6d65066f53b4a025282a5072b6b336c17560b00e04606d/ormsgpack-1.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16c63b0c5a3eec467e4bb33a14dabba076b7d934dff62898297b5c0b5f7c3cb3", size = 202338, upload-time = "2025-12-14T07:56:57.585Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/f5/6d95d7b7c11f97a92522082fc7e5d1ab34537929f1e13f4c369f392f19d0/ormsgpack-1.12.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:74fd6a8e037eb310dda865298e8d122540af00fe5658ec18b97a1d34f4012e4d", size = 210720, upload-time = "2025-12-14T07:56:58.968Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/9d/9a49a2686f8b7165dcb2342b8554951263c30c0f0825f1fcc2d56e736a6b/ormsgpack-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58ad60308e233dd824a1859eabb5fe092e123e885eafa4ad5789322329c80fb5", size = 211264, upload-time = "2025-12-14T07:57:00.099Z" },
+ { url = "https://files.pythonhosted.org/packages/02/31/2fdc36eaeca2182900b96fc7b19755f293283fe681750e3d295733d62f0e/ormsgpack-1.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:35127464c941c1219acbe1a220e48d55e7933373d12257202f4042f7044b4c90", size = 386081, upload-time = "2025-12-14T07:57:01.177Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/65/0a765432f08ae26b4013c6a9aed97be17a9ef85f1600948a474b518e27dd/ormsgpack-1.12.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c48d1c50794692d1e6e3f8c3bb65f5c3acfaae9347e506484a65d60b3d91fb50", size = 479572, upload-time = "2025-12-14T07:57:02.738Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/4f/f2f15ebef786ad71cea420bf8692448fbddf04d1bf3feaa68bd5ee3172e6/ormsgpack-1.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b512b2ad6feaaefdc26e05431ed2843e42483041e354e167c53401afaa83d919", size = 387862, upload-time = "2025-12-14T07:57:03.842Z" },
+ { url = "https://files.pythonhosted.org/packages/15/eb/86fbef1d605fa91ecef077f93f9d0e34fc39b23475dfe3ffb92f6c8db28d/ormsgpack-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:93f30db95e101a9616323bfc50807ad00e7f6197cea2216d2d24af42afc77d88", size = 115900, upload-time = "2025-12-14T07:57:05.137Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/67/7ba1a46e6a6e263fc42a4fafc24afc1ab21a66116553cad670426f0bd9ef/ormsgpack-1.12.1-cp311-cp311-win_arm64.whl", hash = "sha256:d75b5fa14f6abffce2c392ee03b4731199d8a964c81ee8645c4c79af0e80fd50", size = 109868, upload-time = "2025-12-14T07:57:06.834Z" },
+ { url = "https://files.pythonhosted.org/packages/17/fe/ab9167ca037406b5703add24049cf3e18021a3b16133ea20615b1f160ea4/ormsgpack-1.12.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4d7fb0e1b6fbc701d75269f7405a4f79230a6ce0063fb1092e4f6577e312f86d", size = 376725, upload-time = "2025-12-14T07:57:07.894Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/ea/2820e65f506894c459b840d1091ae6e327fde3d5a3f3b002a11a1b9bdf7d/ormsgpack-1.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43a9353e2db5b024c91a47d864ef15eaa62d81824cfc7740fed4cef7db738694", size = 202466, upload-time = "2025-12-14T07:57:09.049Z" },
+ { url = "https://files.pythonhosted.org/packages/45/8b/def01c13339c5bbec2ee1469ef53e7fadd66c8d775df974ee4def1572515/ormsgpack-1.12.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc8fe866b7706fc25af0adf1f600bc06ece5b15ca44e34641327198b821e5c3c", size = 210748, upload-time = "2025-12-14T07:57:10.074Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/d2/bf350c92f7f067dd9484499705f2d8366d8d9008a670e3d1d0add1908f85/ormsgpack-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:813755b5f598a78242042e05dfd1ada4e769e94b98c9ab82554550f97ff4d641", size = 211510, upload-time = "2025-12-14T07:57:11.165Z" },
+ { url = "https://files.pythonhosted.org/packages/74/92/9d689bcb95304a6da26c4d59439c350940c25d1b35f146d402ccc6344c51/ormsgpack-1.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8eea2a13536fae45d78f93f2cc846c9765c7160c85f19cfefecc20873c137cdd", size = 386237, upload-time = "2025-12-14T07:57:12.306Z" },
+ { url = "https://files.pythonhosted.org/packages/17/fe/bd3107547f8b6129265dd957f40b9cd547d2445db2292aacb13335a7ea89/ormsgpack-1.12.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7a02ebda1a863cbc604740e76faca8eee1add322db2dcbe6cf32669fffdff65c", size = 479589, upload-time = "2025-12-14T07:57:13.475Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/7c/e8e5cc9edb967d44f6f85e9ebdad440b59af3fae00b137a4327dc5aed9bb/ormsgpack-1.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c0bd63897c439931cdf29348e5e6e8c330d529830e848d10767615c0f3d1b82", size = 388077, upload-time = "2025-12-14T07:57:14.551Z" },
+ { url = "https://files.pythonhosted.org/packages/35/6b/5031797e43b58506f28a8760b26dc23f2620fb4f2200c4c1b3045603e67e/ormsgpack-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:362f2e812f8d7035dc25a009171e09d7cc97cb30d3c9e75a16aeae00ca3c1dcf", size = 116190, upload-time = "2025-12-14T07:57:15.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/fd/9f43ea6425e383a6b2dbfafebb06fd60e8d68c700ef715adfbcdb499f75d/ormsgpack-1.12.1-cp312-cp312-win_arm64.whl", hash = "sha256:6190281e381db2ed0045052208f47a995ccf61eed48f1215ae3cce3fbccd59c5", size = 109990, upload-time = "2025-12-14T07:57:16.419Z" },
+ { url = "https://files.pythonhosted.org/packages/11/42/f110dfe7cf23a52a82e23eb23d9a6a76ae495447d474686dfa758f3d71d6/ormsgpack-1.12.1-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9663d6b3ecc917c063d61a99169ce196a80f3852e541ae404206836749459279", size = 376746, upload-time = "2025-12-14T07:57:17.699Z" },
+ { url = "https://files.pythonhosted.org/packages/11/76/b386e508a8ae207daec240201a81adb26467bf99b163560724e86bd9ff33/ormsgpack-1.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32e85cfbaf01a94a92520e7fe7851cfcfe21a5698299c28ab86194895f9b9233", size = 202489, upload-time = "2025-12-14T07:57:18.807Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/0e/5db7a63f387149024572daa3d9512fe8fb14bf4efa0722d6d491bed280e7/ormsgpack-1.12.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dabfd2c24b59c7c69870a5ecee480dfae914a42a0c2e7c9d971cf531e2ba471a", size = 210757, upload-time = "2025-12-14T07:57:19.893Z" },
+ { url = "https://files.pythonhosted.org/packages/64/79/3a9899e57cb57430bd766fc1b4c9ad410cb2ba6070bc8cf6301e7d385768/ormsgpack-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51bbf2b64afeded34ccd8e25402e4bca038757913931fa0d693078d75563f6f9", size = 211518, upload-time = "2025-12-14T07:57:20.972Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/cd/4f41710ae9fe50d7fcbe476793b3c487746d0e1cc194cc0fee42ff6d989b/ormsgpack-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9959a71dde1bd0ced84af17facc06a8afada495a34e9cb1bad8e9b20d4c59cef", size = 386251, upload-time = "2025-12-14T07:57:22.099Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/54/ba0c97d6231b1f01daafaa520c8cce1e1b7fceaae6fdc1c763925874a7de/ormsgpack-1.12.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:e9be0e3b62d758f21f5b20e0e06b3a240ec546c4a327bf771f5825462aa74714", size = 479607, upload-time = "2025-12-14T07:57:23.525Z" },
+ { url = "https://files.pythonhosted.org/packages/18/75/19a9a97a462776d525baf41cfb7072734528775f0a3d5fbfab3aa7756b9b/ormsgpack-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a29d49ab7fdd77ea787818e60cb4ef491708105b9c4c9b0f919201625eb036b5", size = 388062, upload-time = "2025-12-14T07:57:24.616Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/6a/ec26e3f44e9632ecd2f43638b7b37b500eaea5d79cab984ad0b94be14f82/ormsgpack-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:c418390b47a1d367e803f6c187f77e4d67c7ae07ba962e3a4a019001f4b0291a", size = 116195, upload-time = "2025-12-14T07:57:25.626Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/64/bfa5f4a34d0f15c6aba1b73e73f7441a66d635bd03249d334a4796b7a924/ormsgpack-1.12.1-cp313-cp313-win_arm64.whl", hash = "sha256:cfa22c91cffc10a7fbd43729baff2de7d9c28cef2509085a704168ae31f02568", size = 109986, upload-time = "2025-12-14T07:57:26.569Z" },
+ { url = "https://files.pythonhosted.org/packages/87/0e/78e5697164e3223b9b216c13e99f1acbc1ee9833490d68842b13da8ba883/ormsgpack-1.12.1-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b93c91efb1a70751a1902a5b43b27bd8fd38e0ca0365cf2cde2716423c15c3a6", size = 376758, upload-time = "2025-12-14T07:57:27.641Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0e/3a3cbb64703263d7bbaed7effa3ce78cb9add360a60aa7c544d7df28b641/ormsgpack-1.12.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf0ea0389167b5fa8d2933dd3f33e887ec4ba68f89c25214d7eec4afd746d22", size = 202487, upload-time = "2025-12-14T07:57:29.051Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/2c/807ebe2b77995599bbb1dec8c3f450d5d7dddee14ce3e1e71dc60e2e2a74/ormsgpack-1.12.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4c29af837f35af3375070689e781161e7cf019eb2f7cd641734ae45cd001c0d", size = 210853, upload-time = "2025-12-14T07:57:30.508Z" },
+ { url = "https://files.pythonhosted.org/packages/25/57/2cdfc354e3ad8e847628f511f4d238799d90e9e090941e50b9d5ba955ae2/ormsgpack-1.12.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:336fc65aa0fe65896a3dabaae31e332a0a98b4a00ad7b0afde21a7505fd23ff3", size = 211545, upload-time = "2025-12-14T07:57:31.585Z" },
+ { url = "https://files.pythonhosted.org/packages/76/1d/c6fda560e4a8ff865b3aec8a86f7c95ab53f4532193a6ae4ab9db35f85aa/ormsgpack-1.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:940f60aabfefe71dd6b82cb33f4ff10b2e7f5fcfa5f103cdb0a23b6aae4c713c", size = 386333, upload-time = "2025-12-14T07:57:32.957Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/3e/715081b36fceb8b497c68b87d384e1cc6d9c9c130ce3b435634d3d785b86/ormsgpack-1.12.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:596ad9e1b6d4c95595c54aaf49b1392609ca68f562ce06f4f74a5bc4053bcda4", size = 479701, upload-time = "2025-12-14T07:57:34.686Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/cf/01ad04def42b3970fc1a302c07f4b46339edf62ef9650247097260471f40/ormsgpack-1.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:575210e8fcbc7b0375026ba040a5eef223e9f66a4453d9623fc23282ae09c3c8", size = 388148, upload-time = "2025-12-14T07:57:35.771Z" },
+ { url = "https://files.pythonhosted.org/packages/15/91/1fff2fc2b5943c740028f339154e7103c8f2edf1a881d9fbba2ce11c3b1d/ormsgpack-1.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:647daa3718572280893456be44c60aea6690b7f2edc54c55648ee66e8f06550f", size = 116201, upload-time = "2025-12-14T07:57:36.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/66/142b542aed3f96002c7d1c33507ca6e1e0d0a42b9253ab27ef7ed5793bd9/ormsgpack-1.12.1-cp314-cp314-win_arm64.whl", hash = "sha256:a8b3ab762a6deaf1b6490ab46dda0c51528cf8037e0246c40875c6fe9e37b699", size = 110029, upload-time = "2025-12-14T07:57:37.703Z" },
+ { url = "https://files.pythonhosted.org/packages/38/b3/ef4494438c90359e1547eaed3c5ec46e2c431d59a3de2af4e70ebd594c49/ormsgpack-1.12.1-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:12087214e436c1f6c28491949571abea759a63111908c4f7266586d78144d7a8", size = 376777, upload-time = "2025-12-14T07:57:38.795Z" },
+ { url = "https://files.pythonhosted.org/packages/05/a0/1149a7163f8b0dfbc64bf9099b6f16d102ad3b03bcc11afee198d751da2d/ormsgpack-1.12.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6d54c14cf86ef13f10ccade94d1e7de146aa9b17d371e18b16e95f329393b7", size = 202490, upload-time = "2025-12-14T07:57:40.168Z" },
+ { url = "https://files.pythonhosted.org/packages/68/82/f2ec5e758d6a7106645cca9bb7137d98bce5d363789fa94075be6572057c/ormsgpack-1.12.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f3584d07882b7ea2a1a589f795a3af97fe4c2932b739408e6d1d9d286cad862", size = 211733, upload-time = "2025-12-14T07:57:42.253Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "1.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "psycopg"
+version = "3.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+ { name = "tzdata", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e0/1a/7d9ef4fdc13ef7f15b934c393edc97a35c281bb7d3c3329fbfcbe915a7c2/psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7", size = 165630, upload-time = "2025-12-06T17:34:53.899Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8c/51/2779ccdf9305981a06b21a6b27e8547c948d85c41c76ff434192784a4c93/psycopg-3.3.2-py3-none-any.whl", hash = "sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b", size = 212774, upload-time = "2025-12-06T17:31:41.414Z" },
+]
+
+[package.optional-dependencies]
+binary = [
+ { name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
+]
+
+[[package]]
+name = "psycopg-binary"
+version = "3.3.2"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/d7/edfb0d9e56081246fd88490f99b1bafebd3588480cca601a4de0c41a3e08/psycopg_binary-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0768c5f32934bb52a5df098317eca9bdcf411de627c5dca2ee57662b64b54b41", size = 4597785, upload-time = "2025-12-06T17:31:44.867Z" },
+ { url = "https://files.pythonhosted.org/packages/71/45/8458201d9573dd851263a05cefddd4bfd31e8b3c6434b3e38d62aea9f15a/psycopg_binary-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:09b3014013f05cd89828640d3a1db5f829cc24ad8fa81b6e42b2c04685a0c9d4", size = 4664440, upload-time = "2025-12-06T17:31:49.1Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/484260d87456cfe88dc219c1919026f11949b9d1de8a6371ddbe027d4d60/psycopg_binary-3.3.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3789d452a9d17a841c7f4f97bbcba51a21f957ea35641a4c98507520e6b6a068", size = 5478355, upload-time = "2025-12-06T17:31:52.657Z" },
+ { url = "https://files.pythonhosted.org/packages/34/b2/18c91630c30c83f534c2bfa75fb533293fc9c3ab31bb7f2bf1cd9579c53b/psycopg_binary-3.3.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44e89938d36acc4495735af70a886d206a5bfdc80258f95b69b52f68b2968d9e", size = 5152398, upload-time = "2025-12-06T17:31:56.092Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/14/7c705e1934107196d9dca2040cf34bce2ca26de62520e43073d2673052d4/psycopg_binary-3.3.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90ed9da805e52985b0202aed4f352842c907c6b4fc6c7c109c6e646c32e2f43b", size = 6748982, upload-time = "2025-12-06T17:32:00.611Z" },
+ { url = "https://files.pythonhosted.org/packages/56/18/80197c47798926f79e563af02a71d1abecab88cf45ddf8dc960700598da7/psycopg_binary-3.3.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3a9ccdfee4ae59cf9bf1822777e763bc097ed208f4901e21537fca1070e1391", size = 4991214, upload-time = "2025-12-06T17:32:03.897Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/2e/e88e2f678f5d1a968d87e57b30915061c1157e916b8aaa9b0b78bca95e25/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de9173f8cc0efd88ac2a89b3b6c287a9a0011cdc2f53b2a12c28d6fd55f9f81c", size = 4517421, upload-time = "2025-12-06T17:32:07.287Z" },
+ { url = "https://files.pythonhosted.org/packages/80/9e/d56813b24370723bcd62bf73871aee4d5fca0536f3476c4c4d5b037e3c7f/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0611f4822674f3269e507a307236efb62ae5a828fcfc923ac85fe22ca19fd7c8", size = 4206124, upload-time = "2025-12-06T17:32:10.374Z" },
+ { url = "https://files.pythonhosted.org/packages/91/81/5a11a898969edf0ee43d0613a6dfd689a0aa12d418c69e148a8ff153fbc7/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:522b79c7db547767ca923e441c19b97a2157f2f494272a119c854bba4804e186", size = 3937067, upload-time = "2025-12-06T17:32:13.852Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/33/a6180ff1e747a0395876d985e8e295c9d7cbe956a2d66f165e7c67cffe55/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ea41c0229f3f5a3844ad0857a83a9f869aa7b840448fa0c200e6bcf85d33d19", size = 4243731, upload-time = "2025-12-06T17:32:16.803Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/5b/9c1b6fbc900d5b525946ed9a477865c5016a5306080c0557248bb04f1a5b/psycopg_binary-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:8ea05b499278790a8fa0ff9854ab0de2542aca02d661ddff94e830df971ff640", size = 3546403, upload-time = "2025-12-06T17:32:19.621Z" },
+ { url = "https://files.pythonhosted.org/packages/57/d9/49640360fc090d27afc4655021544aa71d5393ebae124ffa53a04474b493/psycopg_binary-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:94503b79f7da0b65c80d0dbb2f81dd78b300319ec2435d5e6dcf9622160bc2fa", size = 4597890, upload-time = "2025-12-06T17:32:23.087Z" },
+ { url = "https://files.pythonhosted.org/packages/85/cf/99634bbccc8af0dd86df4bce705eea5540d06bb7f5ab3067446ae9ffdae4/psycopg_binary-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07a5f030e0902ec3e27d0506ceb01238c0aecbc73ecd7fa0ee55f86134600b5b", size = 4664396, upload-time = "2025-12-06T17:32:26.421Z" },
+ { url = "https://files.pythonhosted.org/packages/40/db/6035dff6d5c6dfca3a4ab0d2ac62ede623646e327e9f99e21e0cf08976c6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e09d0d93d35c134704a2cb2b15f81ffc8174fd602f3e08f7b1a3d8896156cf0", size = 5478743, upload-time = "2025-12-06T17:32:29.901Z" },
+ { url = "https://files.pythonhosted.org/packages/03/0f/fc06bbc8e87f09458d2ce04a59cd90565e54e8efca33e0802daee6d2b0e6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:649c1d33bedda431e0c1df646985fbbeb9274afa964e1aef4be053c0f23a2924", size = 5151820, upload-time = "2025-12-06T17:32:33.562Z" },
+ { url = "https://files.pythonhosted.org/packages/86/ab/bcc0397c96a0ad29463e33ed03285826e0fabc43595c195f419d9291ee70/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5774272f754605059521ff037a86e680342e3847498b0aa86b0f3560c70963c", size = 6747711, upload-time = "2025-12-06T17:32:38.074Z" },
+ { url = "https://files.pythonhosted.org/packages/96/eb/7450bc75c31d5be5f7a6d02d26beef6989a4ca6f5efdec65eea6cf612d0e/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d391b70c9cc23f6e1142729772a011f364199d2c5ddc0d596f5f43316fbf982d", size = 4991626, upload-time = "2025-12-06T17:32:41.373Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/85/65f14453804c82a7fba31cd1a984b90349c0f327b809102c4b99115c0930/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f3f601f32244a677c7b029ec39412db2772ad04a28bc2cbb4b1f0931ed0ffad7", size = 4516760, upload-time = "2025-12-06T17:32:44.921Z" },
+ { url = "https://files.pythonhosted.org/packages/24/8c/3105f00a91d73d9a443932f95156eae8159d5d9cb68a9d2cf512710d484f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0ae60e910531cfcc364a8f615a7941cac89efeb3f0fffe0c4824a6d11461eef7", size = 4204028, upload-time = "2025-12-06T17:32:48.355Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/dd/74f64a383342ef7c22d1eb2768ed86411c7f877ed2580cd33c17f436fe3c/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c43a773dd1a481dbb2fe64576aa303d80f328cce0eae5e3e4894947c41d1da7", size = 3935780, upload-time = "2025-12-06T17:32:51.347Z" },
+ { url = "https://files.pythonhosted.org/packages/85/30/f3f207d1c292949a26cdea6727c9c325b4ee41e04bf2736a4afbe45eb61f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a327327f1188b3fbecac41bf1973a60b86b2eb237db10dc945bd3dc97ec39e4", size = 4243239, upload-time = "2025-12-06T17:32:54.924Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/08/8f1b5d6231338bf7bc46f635c4d4965facec52e1c9a7952ca8a70cb57dc0/psycopg_binary-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:136c43f185244893a527540307167f5d3ef4e08786508afe45d6f146228f5aa9", size = 3548102, upload-time = "2025-12-06T17:32:57.944Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/1e/8614b01c549dd7e385dacdcd83fe194f6b3acb255a53cc67154ee6bf00e7/psycopg_binary-3.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9387ab615f929e71ef0f4a8a51e986fa06236ccfa9f3ec98a88f60fbf230634", size = 4579832, upload-time = "2025-12-06T17:33:01.388Z" },
+ { url = "https://files.pythonhosted.org/packages/26/97/0bb093570fae2f4454d42c1ae6000f15934391867402f680254e4a7def54/psycopg_binary-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3ff7489df5e06c12d1829544eaec64970fe27fe300f7cf04c8495fe682064688", size = 4658786, upload-time = "2025-12-06T17:33:05.022Z" },
+ { url = "https://files.pythonhosted.org/packages/61/20/1d9383e3f2038826900a14137b0647d755f67551aab316e1021443105ed5/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9742580ecc8e1ac45164e98d32ca6df90da509c2d3ff26be245d94c430f92db4", size = 5454896, upload-time = "2025-12-06T17:33:09.023Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/513c80ad8bbb545e364f7737bf2492d34a4c05eef4f7b5c16428dc42260d/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d45acedcaa58619355f18e0f42af542fcad3fd84ace4b8355d3a5dea23318578", size = 5132731, upload-time = "2025-12-06T17:33:12.519Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/28/ddf5f5905f088024bccb19857949467407c693389a14feb527d6171d8215/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d88f32ff8c47cb7f4e7e7a9d1747dcee6f3baa19ed9afa9e5694fd2fb32b61ed", size = 6724495, upload-time = "2025-12-06T17:33:16.624Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/93/a1157ebcc650960b264542b547f7914d87a42ff0cc15a7584b29d5807e6b/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59d0163c4617a2c577cb34afbed93d7a45b8c8364e54b2bd2020ff25d5f5f860", size = 4964979, upload-time = "2025-12-06T17:33:20.179Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/27/65939ba6798f9c5be4a5d9cd2061ebaf0851798525c6811d347821c8132d/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e750afe74e6c17b2c7046d2c3e3173b5a3f6080084671c8aa327215323df155b", size = 4493648, upload-time = "2025-12-06T17:33:23.464Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/c4/5e9e4b9b1c1e27026e43387b0ba4aaf3537c7806465dd3f1d5bde631752a/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f26f113013c4dcfbfe9ced57b5bad2035dda1a7349f64bf726021968f9bccad3", size = 4173392, upload-time = "2025-12-06T17:33:26.88Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/81/cf43fb76993190cee9af1cbcfe28afb47b1928bdf45a252001017e5af26e/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8309ee4569dced5e81df5aa2dcd48c7340c8dee603a66430f042dfbd2878edca", size = 3909241, upload-time = "2025-12-06T17:33:30.092Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/20/c6377a0d17434674351627489deca493ea0b137c522b99c81d3a106372c8/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6464150e25b68ae3cb04c4e57496ea11ebfaae4d98126aea2f4702dd43e3c12", size = 4219746, upload-time = "2025-12-06T17:33:33.097Z" },
+ { url = "https://files.pythonhosted.org/packages/25/32/716c57b28eefe02a57a4c9d5bf956849597f5ea476c7010397199e56cfde/psycopg_binary-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:716a586f99bbe4f710dc58b40069fcb33c7627e95cc6fc936f73c9235e07f9cf", size = 3537494, upload-time = "2025-12-06T17:33:35.82Z" },
+ { url = "https://files.pythonhosted.org/packages/14/73/7ca7cb22b9ac7393fb5de7d28ca97e8347c375c8498b3bff2c99c1f38038/psycopg_binary-3.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc5a189e89cbfff174588665bb18d28d2d0428366cc9dae5864afcaa2e57380b", size = 4579068, upload-time = "2025-12-06T17:33:39.303Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/42/0cf38ff6c62c792fc5b55398a853a77663210ebd51ed6f0c4a05b06f95a6/psycopg_binary-3.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:083c2e182be433f290dc2c516fd72b9b47054fcd305cce791e0a50d9e93e06f2", size = 4657520, upload-time = "2025-12-06T17:33:42.536Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/60/df846bc84cbf2231e01b0fff48b09841fe486fa177665e50f4995b1bfa44/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ac230e3643d1c436a2dfb59ca84357dfc6862c9f372fc5dbd96bafecae581f9f", size = 5452086, upload-time = "2025-12-06T17:33:46.54Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/85/30c846a00db86b1b53fd5bfd4b4edfbd0c00de8f2c75dd105610bd7568fc/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d8c899a540f6c7585cee53cddc929dd4d2db90fd828e37f5d4017b63acbc1a5d", size = 5131125, upload-time = "2025-12-06T17:33:50.413Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/15/9968732013373f36f8a2a3fb76104dffc8efd9db78709caa5ae1a87b1f80/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50ff10ab8c0abdb5a5451b9315538865b50ba64c907742a1385fdf5f5772b73e", size = 6722914, upload-time = "2025-12-06T17:33:54.544Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/ba/29e361fe02143ac5ff5a1ca3e45697344cfbebe2eaf8c4e7eec164bff9a0/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23d2594af848c1fd3d874a9364bef50730124e72df7bb145a20cb45e728c50ed", size = 4966081, upload-time = "2025-12-06T17:33:58.477Z" },
+ { url = "https://files.pythonhosted.org/packages/99/45/1be90c8f1a1a237046903e91202fb06708745c179f220b361d6333ed7641/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea4fe6b4ead3bbbe27244ea224fcd1f53cb119afc38b71a2f3ce570149a03e30", size = 4493332, upload-time = "2025-12-06T17:34:02.011Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/b5/bbdc07d5f0a5e90c617abd624368182aa131485e18038b2c6c85fc054aed/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:742ce48cde825b8e52fb1a658253d6d1ff66d152081cbc76aa45e2986534858d", size = 4170781, upload-time = "2025-12-06T17:34:05.298Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/2a/0d45e4f4da2bd78c3237ffa03475ef3751f69a81919c54a6e610eb1a7c96/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e22bf6b54df994aff37ab52695d635f1ef73155e781eee1f5fa75bc08b58c8da", size = 3910544, upload-time = "2025-12-06T17:34:08.251Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/62/a8e0f092f4dbef9a94b032fb71e214cf0a375010692fbe7493a766339e47/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8db9034cde3bcdafc66980f0130813f5c5d19e74b3f2a19fb3cfbc25ad113121", size = 4220070, upload-time = "2025-12-06T17:34:11.392Z" },
+ { url = "https://files.pythonhosted.org/packages/09/e6/5fc8d8aff8afa114bb4a94a0341b9309311e8bf3ab32d816032f8b984d4e/psycopg_binary-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:df65174c7cf6b05ea273ce955927d3270b3a6e27b0b12762b009ce6082b8d3fc", size = 3540922, upload-time = "2025-12-06T17:34:14.88Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/75/ad18c0b97b852aba286d06befb398cc6d383e9dfd0a518369af275a5a526/psycopg_binary-3.3.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9ca24062cd9b2270e4d77576042e9cc2b1d543f09da5aba1f1a3d016cea28390", size = 4596371, upload-time = "2025-12-06T17:34:18.007Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/79/91649d94c8d89f84af5da7c9d474bfba35b08eb8f492ca3422b08f0a6427/psycopg_binary-3.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c749770da0947bc972e512f35366dd4950c0e34afad89e60b9787a37e97cb443", size = 4675139, upload-time = "2025-12-06T17:34:21.374Z" },
+ { url = "https://files.pythonhosted.org/packages/56/ac/b26e004880f054549ec9396594e1ffe435810b0673e428e619ed722e4244/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03b7cd73fb8c45d272a34ae7249713e32492891492681e3cf11dff9531cf37e9", size = 5456120, upload-time = "2025-12-06T17:34:25.102Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/8d/410681dccd6f2999fb115cc248521ec50dd2b0aba66ae8de7e81efdebbee/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43b130e3b6edcb5ee856c7167ccb8561b473308c870ed83978ae478613764f1c", size = 5133484, upload-time = "2025-12-06T17:34:28.933Z" },
+ { url = "https://files.pythonhosted.org/packages/66/30/ebbab99ea2cfa099d7b11b742ce13415d44f800555bfa4ad2911dc645b71/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1feba5a8c617922321aef945865334e468337b8fc5c73074f5e63143013b5a", size = 6731818, upload-time = "2025-12-06T17:34:33.094Z" },
+ { url = "https://files.pythonhosted.org/packages/70/02/d260646253b7ad805d60e0de47f9b811d6544078452579466a098598b6f4/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cabb2a554d9a0a6bf84037d86ca91782f087dfff2a61298d0b00c19c0bc43f6d", size = 4983859, upload-time = "2025-12-06T17:34:36.457Z" },
+ { url = "https://files.pythonhosted.org/packages/72/8d/e778d7bad1a7910aa36281f092bd85c5702f508fd9bb0ea2020ffbb6585c/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74bc306c4b4df35b09bc8cecf806b271e1c5d708f7900145e4e54a2e5dedfed0", size = 4516388, upload-time = "2025-12-06T17:34:40.129Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/f1/64e82098722e2ab3521797584caf515284be09c1e08a872551b6edbb0074/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d79b0093f0fbf7a962d6a46ae292dc056c65d16a8ee9361f3cfbafd4c197ab14", size = 4192382, upload-time = "2025-12-06T17:34:43.279Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/d0/c20f4e668e89494972e551c31be2a0016e3f50d552d7ae9ac07086407599/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678", size = 3928660, upload-time = "2025-12-06T17:34:46.757Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/e1/99746c171de22539fd5eb1c9ca21dc805b54cfae502d7451d237d1dbc349/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e", size = 4239169, upload-time = "2025-12-06T17:34:49.751Z" },
+ { url = "https://files.pythonhosted.org/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122, upload-time = "2025-12-06T17:34:52.506Z" },
+]
+
+[[package]]
+name = "psycopg-pool"
+version = "3.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/56/9a/9470d013d0d50af0da9c4251614aeb3c1823635cab3edc211e3839db0bcf/psycopg_pool-3.3.0.tar.gz", hash = "sha256:fa115eb2860bd88fce1717d75611f41490dec6135efb619611142b24da3f6db5", size = 31606, upload-time = "2025-12-01T11:34:33.11Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/c3/26b8a0908a9db249de3b4169692e1c7c19048a9bc41a4d3209cee7dbb758/psycopg_pool-3.3.0-py3-none-any.whl", hash = "sha256:2e44329155c410b5e8666372db44276a8b1ebd8c90f1c3026ebba40d4bc81063", size = 39995, upload-time = "2025-12-01T11:34:29.761Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" },
+ { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" },
+ { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" },
+ { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+ { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
+ { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" },
+ { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" },
+ { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
+ { name = "pytest" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
+]
+
+[[package]]
+name = "pytest-mock"
+version = "3.15.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
+]
+
+[[package]]
+name = "pytest-watcher"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "watchdog" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e6/d2/80606077b7fa8784417687f494ff801d7ab817d9a17fc94305811d5919bb/pytest_watcher-0.6.3.tar.gz", hash = "sha256:842dc904264df0ad2d5264153a66bb452fccfa46598cd6e0a5ef1d19afed9b13", size = 601878, upload-time = "2026-01-10T23:28:18.805Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/3f/172d73600ad2771774cda108efb813fc724fc345e5240a81a1085f1ade5d/pytest_watcher-0.6.3-py3-none-any.whl", hash = "sha256:83e7748c933087e8276edb6078663e6afa9926434b4fd8b85cf6b32b1d5bec89", size = 12431, upload-time = "2026-01-10T23:28:17.64Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
+ { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
+ { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+]
+
+[[package]]
+name = "requests-toolbelt"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.14.13"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" },
+ { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" },
+ { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" },
+ { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" },
+ { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" },
+]
+
+[[package]]
+name = "tenacity"
+version = "9.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
+ { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
+ { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
+ { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
+ { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
+ { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
+ { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
+ { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
+ { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
+ { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
+ { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
+ { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
+ { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
+]
+
+[[package]]
+name = "uuid-utils"
+version = "0.13.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/8a/17b11768dcb473d3a255c02ffdd94fbd1b345c906efea0a39124dcbaed52/uuid_utils-0.13.0.tar.gz", hash = "sha256:4c17df6427a9e23a4cd7fb9ee1efb53b8abb078660b9bdb2524ca8595022dfe1", size = 21921, upload-time = "2026-01-08T15:48:10.841Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/b8/d40848ca22781f206c60a1885fc737d2640392bd6b5792d455525accd89c/uuid_utils-0.13.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:83628283e977fb212e756bc055df8fdd2f9f589a2e539ba1abe755b8ce8df7a4", size = 602130, upload-time = "2026-01-08T15:47:34.877Z" },
+ { url = "https://files.pythonhosted.org/packages/40/b9/00a944b8096632ea12638181f8e294abcde3e3b8b5e29b777f809896f6ae/uuid_utils-0.13.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c47638ed6334ab19d80f73664f153b04bbb04ab8ce4298d10da6a292d4d21c47", size = 304213, upload-time = "2026-01-08T15:47:36.807Z" },
+ { url = "https://files.pythonhosted.org/packages/da/d7/07b36c33aef683b81c9afff3ec178d5eb39d325447a68c3c68a62e4abb32/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:b276b538c57733ed406948584912da422a604313c71479654848b84b9e19c9b0", size = 340624, upload-time = "2026-01-08T15:47:38.821Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/55/fcff2fff02a27866cb1a6614c9df2b3ace721f0a0aab2b7b8f5a7d4e4221/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_armv7l.whl", hash = "sha256:bdaf2b77e34b199cf04cde28399495fd1ed951de214a4ece1f3919b2f945bb06", size = 346705, upload-time = "2026-01-08T15:47:40.397Z" },
+ { url = "https://files.pythonhosted.org/packages/41/48/67438506c2bb8bee1b4b00d7c0b3ff866401b4790849bf591d654d4ea0bc/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_i686.whl", hash = "sha256:eb2f0baf81e82f9769a7684022dca8f3bf801ca1574a3e94df1876e9d6f9271e", size = 366023, upload-time = "2026-01-08T15:47:42.662Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/d7/2d91ce17f62fd764d593430de296b70843cc25229c772453f7261de9e6a8/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_ppc64le.whl", hash = "sha256:6be6c4d11275f5cc402a4fdba6c2b1ce45fd3d99bb78716cd1cc2cbf6802b2ce", size = 471149, upload-time = "2026-01-08T15:47:44.963Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/9a/aa0756186073ba84daf5704c150d41ede10eb3185d510e02532e2071550e/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:77621cf6ceca7f42173a642a01c01c216f9eaec3b7b65d093d2d6a433ca0a83d", size = 342130, upload-time = "2026-01-08T15:47:46.331Z" },
+ { url = "https://files.pythonhosted.org/packages/74/b4/3191789f4dc3bed59d79cec90559821756297a25d7dc34d1bf7781577a75/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a5a9eb06c2bb86dd876cd7b2fe927fc8543d14c90d971581db6ffda4a02526f", size = 524128, upload-time = "2026-01-08T15:47:47.628Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/30/29839210a8fff9fc219bfa7c8d8cd115324e92618cba0cda090d54d3d321/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:775347c6110fb71360df17aac74132d8d47c1dbe71233ac98197fc872a791fd2", size = 615872, upload-time = "2026-01-08T15:47:50.61Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ed/15000c96a8bd8f5fd8efd622109bf52549ea0b366f8ce71c45580fa55878/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf95f6370ad1a0910ee7b5ad5228fd19c4ae32fe3627389006adaf519408c41e", size = 581023, upload-time = "2026-01-08T15:47:52.776Z" },
+ { url = "https://files.pythonhosted.org/packages/67/c8/3f809fa2dc2ca4bd331c792a3c7d3e45ae2b709d85847a12b8b27d1d5f19/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a88e23e0b2f4203fefe2ccbca5736ee06fcad10e61b5e7e39c8d7904bc13300", size = 546715, upload-time = "2026-01-08T15:47:54.415Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/80/4f7c7efd734d1494397c781bd3d421688e9c187ae836e3174625b1ddf8b0/uuid_utils-0.13.0-cp39-abi3-win32.whl", hash = "sha256:3e4f2cc54e6a99c0551158100ead528479ad2596847478cbad624977064ffce3", size = 177650, upload-time = "2026-01-08T15:47:55.679Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/94/d05ab68622e66ad787a241dfe5ccc649b3af09f30eae977b9ee8f7046aaa/uuid_utils-0.13.0-cp39-abi3-win_amd64.whl", hash = "sha256:046cb2756e1597b3de22d24851b769913e192135830486a0a70bf41327f0360c", size = 183211, upload-time = "2026-01-08T15:47:57.604Z" },
+ { url = "https://files.pythonhosted.org/packages/69/37/674b3ce25cd715b831ea8ebbd828b74c40159f04c95d1bb963b2c876fe79/uuid_utils-0.13.0-cp39-abi3-win_arm64.whl", hash = "sha256:5447a680df6ef8a5a353976aaf4c97cc3a3a22b1ee13671c44227b921e3ae2a9", size = 183518, upload-time = "2026-01-08T15:47:59.148Z" },
+ { url = "https://files.pythonhosted.org/packages/99/fa/1d92de9538463859228e68db679b766fd300770c9a2db849dcba0c0c5a57/uuid_utils-0.13.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e5182e2d95f38e65f2e5bce90648ef56987443da13e145afcd747e584f9bc69c", size = 587641, upload-time = "2026-01-08T15:48:00.433Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/07/6bd9e6f5367e38c2ee7178ad882d2bd1b0d17c5393974b09ab027a215eba/uuid_utils-0.13.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e3909a8a1fbd79d7c8bdc874eeb83e23ccb7a7cb0aa821a49596cc96c0cce84b", size = 298273, upload-time = "2026-01-08T15:48:02.063Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/14/7061b868a8a6799c8df83768a23f313d4e22075069f01ee3c28fa82aa2c6/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:5dc4c9f749bd2511b8dcbf0891e658d7d86880022963db050722ad7b502b5e22", size = 333618, upload-time = "2026-01-08T15:48:03.503Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/f1/f48c3c9c343c9071ade5f355403e344d817412d9cf379a2d04b181282e74/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_armv7l.whl", hash = "sha256:516adf07f5b2cdb88d50f489c702b5f1a75ae8b2639bfd254f4192d5f7ee261f", size = 339104, upload-time = "2026-01-08T15:48:05.02Z" },
+ { url = "https://files.pythonhosted.org/packages/47/22/8e3142b4baffee77ce533fe956446d3699ec42f1d5252911208cbef4501e/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_i686.whl", hash = "sha256:aeee3bd89e8de6184a3ab778ce19f5ce9ad32849d1be549516e0ddb257562d8d", size = 359503, upload-time = "2026-01-08T15:48:06.347Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/1a/756f1f9e31b15019c87cd2becb1c596351c50967cd143443da38df8818d1/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_ppc64le.whl", hash = "sha256:97985256c2e59b7caa51f5c8515f64d777328562a9c900ec65e9d627baf72737", size = 467480, upload-time = "2026-01-08T15:48:07.681Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/20/a6929e98d9a461ca49e96194a82a1cc3fd5420f3a2f53cbb34fca438549e/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:b7ccaa20e24c5f60f41a69ef571ed820737f9b0ade4cbeef56aaa8f80f5aa475", size = 333610, upload-time = "2026-01-08T15:48:09.375Z" },
+]
+
+[[package]]
+name = "watchdog"
+version = "6.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" },
+ { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" },
+ { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" },
+ { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" },
+ { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
+ { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
+ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
+ { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
+ { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
+ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
+ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
+]
+
+[[package]]
+name = "zstandard"
+version = "0.25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" },
+ { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" },
+ { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" },
+ { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" },
+ { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" },
+ { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" },
+ { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" },
+ { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" },
+ { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" },
+ { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" },
+ { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" },
+ { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" },
+ { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" },
+ { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" },
+ { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" },
+ { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" },
+ { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" },
+ { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" },
+ { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" },
+ { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" },
+ { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" },
+ { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" },
+ { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" },
+ { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" },
+ { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" },
+ { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" },
+]
diff --git a/langgraph/source/libs/checkpoint-sqlite/LICENSE b/langgraph/source/libs/checkpoint-sqlite/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..fc0602feecdd6748623c852ab534e1ca612673c7
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 LangChain, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/langgraph/source/libs/checkpoint-sqlite/Makefile b/langgraph/source/libs/checkpoint-sqlite/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..376c05d74cadc15760b1dcedb4213dedf67ad3a6
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/Makefile
@@ -0,0 +1,37 @@
+.PHONY: test test_watch lint format
+
+######################
+# TESTING AND COVERAGE
+######################
+
+TEST ?= .
+
+test:
+ uv run pytest $(TEST)
+
+test_watch:
+ uv run ptw $(TEST)
+
+######################
+# LINTING AND FORMATTING
+######################
+
+# Define a variable for Python and notebook files.
+PYTHON_FILES=.
+MYPY_CACHE=.mypy_cache
+lint format: PYTHON_FILES=.
+lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --relative --diff-filter=d main . | grep -E '\.py$$|\.ipynb$$')
+lint_package: PYTHON_FILES=langgraph
+lint_tests: PYTHON_FILES=tests
+lint_tests: MYPY_CACHE=.mypy_cache_test
+
+lint lint_diff lint_package lint_tests:
+ uv run ruff check .
+ [ "$(PYTHON_FILES)" = "" ] || uv run ruff format $(PYTHON_FILES) --diff
+ [ "$(PYTHON_FILES)" = "" ] || uv run ruff check --select I $(PYTHON_FILES)
+ [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE)
+ [ "$(PYTHON_FILES)" = "" ] || uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
+
+format format_diff:
+ uv run ruff format $(PYTHON_FILES)
+ uv run ruff check --select I --fix $(PYTHON_FILES)
diff --git a/langgraph/source/libs/checkpoint-sqlite/README.md b/langgraph/source/libs/checkpoint-sqlite/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..48f4e66901cf5d8b47ac20ebf378c5cb3e9197bc
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/README.md
@@ -0,0 +1,88 @@
+# LangGraph SQLite Checkpoint
+
+Implementation of LangGraph CheckpointSaver that uses SQLite DB (both sync and async, via `aiosqlite`)
+
+## Usage
+
+```python
+from langgraph.checkpoint.sqlite import SqliteSaver
+
+write_config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
+read_config = {"configurable": {"thread_id": "1"}}
+
+with SqliteSaver.from_conn_string(":memory:") as checkpointer:
+ checkpoint = {
+ "v": 4,
+ "ts": "2024-07-31T20:14:19.804150+00:00",
+ "id": "1ef4f797-8335-6428-8001-8a1503f9b875",
+ "channel_values": {
+ "my_key": "meow",
+ "node": "node"
+ },
+ "channel_versions": {
+ "__start__": 2,
+ "my_key": 3,
+ "start:node": 3,
+ "node": 3
+ },
+ "versions_seen": {
+ "__input__": {},
+ "__start__": {
+ "__start__": 1
+ },
+ "node": {
+ "start:node": 2
+ }
+ },
+ }
+
+ # store checkpoint
+ checkpointer.put(write_config, checkpoint, {}, {})
+
+ # load checkpoint
+ checkpointer.get(read_config)
+
+ # list checkpoints
+ list(checkpointer.list(read_config))
+```
+
+### Async
+
+```python
+from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
+
+async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
+ checkpoint = {
+ "v": 4,
+ "ts": "2024-07-31T20:14:19.804150+00:00",
+ "id": "1ef4f797-8335-6428-8001-8a1503f9b875",
+ "channel_values": {
+ "my_key": "meow",
+ "node": "node"
+ },
+ "channel_versions": {
+ "__start__": 2,
+ "my_key": 3,
+ "start:node": 3,
+ "node": 3
+ },
+ "versions_seen": {
+ "__input__": {},
+ "__start__": {
+ "__start__": 1
+ },
+ "node": {
+ "start:node": 2
+ }
+ },
+ }
+
+ # store checkpoint
+ await checkpointer.aput(write_config, checkpoint, {}, {})
+
+ # load checkpoint
+ await checkpointer.aget(read_config)
+
+ # list checkpoints
+ [c async for c in checkpointer.alist(read_config)]
+```
diff --git a/langgraph/source/libs/checkpoint-sqlite/__init__.py b/langgraph/source/libs/checkpoint-sqlite/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/checkpoint-sqlite/langgraph/__init__.py b/langgraph/source/libs/checkpoint-sqlite/langgraph/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/langgraph/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/checkpoint-sqlite/langgraph/cache/__init__.py b/langgraph/source/libs/checkpoint-sqlite/langgraph/cache/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/langgraph/cache/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/checkpoint-sqlite/langgraph/cache/sqlite/__init__.py b/langgraph/source/libs/checkpoint-sqlite/langgraph/cache/sqlite/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..cd327bd4876856bababa3604f23c0574d17f4cae
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/langgraph/cache/sqlite/__init__.py
@@ -0,0 +1,120 @@
+from __future__ import annotations
+
+import asyncio
+import datetime
+import sqlite3
+import threading
+from collections.abc import Mapping, Sequence
+
+from langgraph.cache.base import BaseCache, FullKey, Namespace, ValueT
+from langgraph.checkpoint.serde.base import SerializerProtocol
+
+
+class SqliteCache(BaseCache[ValueT]):
+ """File-based cache using SQLite."""
+
+ def __init__(
+ self,
+ *,
+ path: str,
+ serde: SerializerProtocol | None = None,
+ ) -> None:
+ """Initialize the cache with a file path."""
+ super().__init__(serde=serde)
+ # SQLite backing store
+ self._conn = sqlite3.connect(
+ path,
+ check_same_thread=False,
+ )
+ # Serialize access to the shared connection across threads
+ self._lock = threading.RLock()
+ # Better concurrency & atomicity
+ self._conn.execute("PRAGMA journal_mode=WAL;")
+ # Schema: key -> (expiry, encoding, value)
+ self._conn.execute(
+ """CREATE TABLE IF NOT EXISTS cache (
+ ns TEXT,
+ key TEXT,
+ expiry REAL,
+ encoding TEXT NOT NULL,
+ val BLOB NOT NULL,
+ PRIMARY KEY (ns, key)
+ )"""
+ )
+ self._conn.commit()
+
+ def get(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
+ """Get the cached values for the given keys."""
+ with self._lock, self._conn:
+ now = datetime.datetime.now(datetime.timezone.utc).timestamp()
+ if not keys:
+ return {}
+ placeholders = ",".join("(?, ?)" for _ in keys)
+ params: list[str] = []
+ for ns_tuple, key in keys:
+ params.extend((",".join(ns_tuple), key))
+ cursor = self._conn.execute(
+ f"SELECT ns, key, expiry, encoding, val FROM cache WHERE (ns, key) IN ({placeholders})",
+ tuple(params),
+ )
+ values: dict[FullKey, ValueT] = {}
+ rows = cursor.fetchall()
+ for ns, key, expiry, encoding, raw in rows:
+ if expiry is not None and now > expiry:
+ # purge expired entry
+ self._conn.execute(
+ "DELETE FROM cache WHERE (ns, key) = (?, ?)", (ns, key)
+ )
+ continue
+ values[(tuple(ns.split(",")), key)] = self.serde.loads_typed(
+ (encoding, raw)
+ )
+ return values
+
+ async def aget(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
+ """Asynchronously get the cached values for the given keys."""
+ return await asyncio.to_thread(self.get, keys)
+
+ def set(self, mapping: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
+ """Set the cached values for the given keys and TTLs."""
+ with self._lock, self._conn:
+ now = datetime.datetime.now(datetime.timezone.utc)
+ for key, (value, ttl) in mapping.items():
+ if ttl is not None:
+ delta = datetime.timedelta(seconds=ttl)
+ expiry: float | None = (now + delta).timestamp()
+ else:
+ expiry = None
+ encoding, raw = self.serde.dumps_typed(value)
+ self._conn.execute(
+ "INSERT OR REPLACE INTO cache (ns, key, expiry, encoding, val) VALUES (?, ?, ?, ?, ?)",
+ (",".join(key[0]), key[1], expiry, encoding, raw),
+ )
+
+ async def aset(self, mapping: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
+ """Asynchronously set the cached values for the given keys and TTLs."""
+ await asyncio.to_thread(self.set, mapping)
+
+ def clear(self, namespaces: Sequence[Namespace] | None = None) -> None:
+ """Delete the cached values for the given namespaces.
+ If no namespaces are provided, clear all cached values."""
+ with self._lock, self._conn:
+ if namespaces is None:
+ self._conn.execute("DELETE FROM cache")
+ else:
+ placeholders = ",".join("?" for _ in namespaces)
+ self._conn.execute(
+ f"DELETE FROM cache WHERE (ns) IN ({placeholders})",
+ tuple(",".join(key) for key in namespaces),
+ )
+
+ async def aclear(self, namespaces: Sequence[Namespace] | None = None) -> None:
+ """Asynchronously delete the cached values for the given namespaces.
+ If no namespaces are provided, clear all cached values."""
+ await asyncio.to_thread(self.clear, namespaces)
+
+ def __del__(self) -> None:
+ try:
+ self._conn.close()
+ except Exception:
+ pass
diff --git a/langgraph/source/libs/checkpoint-sqlite/langgraph/checkpoint/__init__.py b/langgraph/source/libs/checkpoint-sqlite/langgraph/checkpoint/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/langgraph/checkpoint/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py b/langgraph/source/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..2684ce16476f8dc844e5cf310c2827e95bbdd199
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py
@@ -0,0 +1,556 @@
+from __future__ import annotations
+
+import json
+import random
+import sqlite3
+import threading
+from collections.abc import AsyncIterator, Iterator, Sequence
+from contextlib import closing, contextmanager
+from typing import Any, cast
+
+from langchain_core.runnables import RunnableConfig
+from langgraph.checkpoint.base import (
+ WRITES_IDX_MAP,
+ BaseCheckpointSaver,
+ ChannelVersions,
+ Checkpoint,
+ CheckpointMetadata,
+ CheckpointTuple,
+ SerializerProtocol,
+ get_checkpoint_id,
+ get_checkpoint_metadata,
+)
+from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
+
+from langgraph.checkpoint.sqlite.utils import search_where
+
+_AIO_ERROR_MSG = (
+ "The SqliteSaver does not support async methods. "
+ "Consider using AsyncSqliteSaver instead.\n"
+ "from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver\n"
+ "Note: AsyncSqliteSaver requires the aiosqlite package to use.\n"
+ "Install with:\n`pip install aiosqlite`\n"
+ "See https://langchain-ai.github.io/langgraph/reference/checkpoints/#langgraph.checkpoint.sqlite.aio.AsyncSqliteSaver"
+ "for more information."
+)
+
+
+class SqliteSaver(BaseCheckpointSaver[str]):
+ """A checkpoint saver that stores checkpoints in a SQLite database.
+
+ Note:
+ This class is meant for lightweight, synchronous use cases
+ (demos and small projects) and does not
+ scale to multiple threads.
+ For a similar sqlite saver with `async` support,
+ consider using [AsyncSqliteSaver][langgraph.checkpoint.sqlite.aio.AsyncSqliteSaver].
+
+ Args:
+ conn (sqlite3.Connection): The SQLite database connection.
+ serde (Optional[SerializerProtocol]): The serializer to use for serializing and deserializing checkpoints. Defaults to JsonPlusSerializerCompat.
+
+ Examples:
+
+ >>> import sqlite3
+ >>> from langgraph.checkpoint.sqlite import SqliteSaver
+ >>> from langgraph.graph import StateGraph
+ >>>
+ >>> builder = StateGraph(int)
+ >>> builder.add_node("add_one", lambda x: x + 1)
+ >>> builder.set_entry_point("add_one")
+ >>> builder.set_finish_point("add_one")
+ >>> # Create a new SqliteSaver instance
+ >>> # Note: check_same_thread=False is OK as the implementation uses a lock
+ >>> # to ensure thread safety.
+ >>> conn = sqlite3.connect("checkpoints.sqlite", check_same_thread=False)
+ >>> memory = SqliteSaver(conn)
+ >>> graph = builder.compile(checkpointer=memory)
+ >>> config = {"configurable": {"thread_id": "1"}}
+ >>> graph.get_state(config)
+ >>> result = graph.invoke(3, config)
+ >>> graph.get_state(config)
+ StateSnapshot(values=4, next=(), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '0c62ca34-ac19-445d-bbb0-5b4984975b2a'}}, parent_config=None)
+ """ # noqa
+
+ conn: sqlite3.Connection
+ is_setup: bool
+
+ def __init__(
+ self,
+ conn: sqlite3.Connection,
+ *,
+ serde: SerializerProtocol | None = None,
+ ) -> None:
+ super().__init__(serde=serde)
+ self.jsonplus_serde = JsonPlusSerializer()
+ self.conn = conn
+ self.is_setup = False
+ self.lock = threading.Lock()
+
+ @classmethod
+ @contextmanager
+ def from_conn_string(cls, conn_string: str) -> Iterator[SqliteSaver]:
+ """Create a new SqliteSaver instance from a connection string.
+
+ Args:
+ conn_string: The SQLite connection string.
+
+ Yields:
+ SqliteSaver: A new SqliteSaver instance.
+
+ Examples:
+
+ In memory:
+
+ with SqliteSaver.from_conn_string(":memory:") as memory:
+ ...
+
+ To disk:
+
+ with SqliteSaver.from_conn_string("checkpoints.sqlite") as memory:
+ ...
+ """
+ with closing(
+ sqlite3.connect(
+ conn_string,
+ # https://ricardoanderegg.com/posts/python-sqlite-thread-safety/
+ check_same_thread=False,
+ )
+ ) as conn:
+ yield cls(conn)
+
+ def setup(self) -> None:
+ """Set up the checkpoint database.
+
+ This method creates the necessary tables in the SQLite database if they don't
+ already exist. It is called automatically when needed and should not be called
+ directly by the user.
+ """
+ if self.is_setup:
+ return
+
+ self.conn.executescript(
+ """
+ PRAGMA journal_mode=WAL;
+ CREATE TABLE IF NOT EXISTS checkpoints (
+ thread_id TEXT NOT NULL,
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
+ checkpoint_id TEXT NOT NULL,
+ parent_checkpoint_id TEXT,
+ type TEXT,
+ checkpoint BLOB,
+ metadata BLOB,
+ PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
+ );
+ CREATE TABLE IF NOT EXISTS writes (
+ thread_id TEXT NOT NULL,
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
+ checkpoint_id TEXT NOT NULL,
+ task_id TEXT NOT NULL,
+ idx INTEGER NOT NULL,
+ channel TEXT NOT NULL,
+ type TEXT,
+ value BLOB,
+ PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
+ );
+ """
+ )
+
+ self.is_setup = True
+
+ @contextmanager
+ def cursor(self, transaction: bool = True) -> Iterator[sqlite3.Cursor]:
+ """Get a cursor for the SQLite database.
+
+ This method returns a cursor for the SQLite database. It is used internally
+ by the SqliteSaver and should not be called directly by the user.
+
+ Args:
+ transaction (bool): Whether to commit the transaction when the cursor is closed. Defaults to True.
+
+ Yields:
+ sqlite3.Cursor: A cursor for the SQLite database.
+ """
+ with self.lock:
+ self.setup()
+ cur = self.conn.cursor()
+ try:
+ yield cur
+ finally:
+ if transaction:
+ self.conn.commit()
+ cur.close()
+
+ def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
+ """Get a checkpoint tuple from the database.
+
+ This method retrieves a checkpoint tuple from the SQLite database based on the
+ provided config. If the config contains a `checkpoint_id` key, the checkpoint with
+ the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
+ for the given thread ID is retrieved.
+
+ Args:
+ config: The config to use for retrieving the checkpoint.
+
+ Returns:
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
+
+ Examples:
+
+ Basic:
+ >>> config = {"configurable": {"thread_id": "1"}}
+ >>> checkpoint_tuple = memory.get_tuple(config)
+ >>> print(checkpoint_tuple)
+ CheckpointTuple(...)
+
+ With checkpoint ID:
+
+ >>> config = {
+ ... "configurable": {
+ ... "thread_id": "1",
+ ... "checkpoint_ns": "",
+ ... "checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875",
+ ... }
+ ... }
+ >>> checkpoint_tuple = memory.get_tuple(config)
+ >>> print(checkpoint_tuple)
+ CheckpointTuple(...)
+ """ # noqa
+ checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
+ with self.cursor(transaction=False) as cur:
+ # find the latest checkpoint for the thread_id
+ if checkpoint_id := get_checkpoint_id(config):
+ cur.execute(
+ "SELECT thread_id, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata FROM checkpoints WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?",
+ (
+ str(config["configurable"]["thread_id"]),
+ checkpoint_ns,
+ checkpoint_id,
+ ),
+ )
+ else:
+ cur.execute(
+ "SELECT thread_id, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata FROM checkpoints WHERE thread_id = ? AND checkpoint_ns = ? ORDER BY checkpoint_id DESC LIMIT 1",
+ (str(config["configurable"]["thread_id"]), checkpoint_ns),
+ )
+ # if a checkpoint is found, return it
+ if value := cur.fetchone():
+ (
+ thread_id,
+ checkpoint_id,
+ parent_checkpoint_id,
+ type,
+ checkpoint,
+ metadata,
+ ) = value
+ if not get_checkpoint_id(config):
+ config = {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": checkpoint_id,
+ }
+ }
+ # find any pending writes
+ cur.execute(
+ "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx",
+ (
+ str(config["configurable"]["thread_id"]),
+ checkpoint_ns,
+ str(config["configurable"]["checkpoint_id"]),
+ ),
+ )
+ # deserialize the checkpoint and metadata
+ return CheckpointTuple(
+ config,
+ self.serde.loads_typed((type, checkpoint)),
+ cast(
+ CheckpointMetadata,
+ json.loads(metadata) if metadata is not None else {},
+ ),
+ (
+ {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": parent_checkpoint_id,
+ }
+ }
+ if parent_checkpoint_id
+ else None
+ ),
+ [
+ (task_id, channel, self.serde.loads_typed((type, value)))
+ for task_id, channel, type, value in cur
+ ],
+ )
+
+ def list(
+ self,
+ config: RunnableConfig | None,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> Iterator[CheckpointTuple]:
+ """List checkpoints from the database.
+
+ This method retrieves a list of checkpoint tuples from the SQLite database based
+ on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
+
+ Args:
+ config: The config to use for listing the checkpoints.
+ filter: Additional filtering criteria for metadata.
+ before: If provided, only checkpoints before the specified checkpoint ID are returned.
+ limit: The maximum number of checkpoints to return.
+
+ Yields:
+ An iterator of checkpoint tuples.
+
+ Examples:
+ >>> from langgraph.checkpoint.sqlite import SqliteSaver
+ >>> with SqliteSaver.from_conn_string(":memory:") as memory:
+ ... # Run a graph, then list the checkpoints
+ >>> config = {"configurable": {"thread_id": "1"}}
+ >>> checkpoints = list(memory.list(config, limit=2))
+ >>> print(checkpoints)
+ [CheckpointTuple(...), CheckpointTuple(...)]
+
+ >>> config = {"configurable": {"thread_id": "1"}}
+ >>> before = {"configurable": {"checkpoint_id": "1ef4f797-8335-6428-8001-8a1503f9b875"}}
+ >>> with SqliteSaver.from_conn_string(":memory:") as memory:
+ ... # Run a graph, then list the checkpoints
+ >>> checkpoints = list(memory.list(config, before=before))
+ >>> print(checkpoints)
+ [CheckpointTuple(...), ...]
+ """
+ where, param_values = search_where(config, filter, before)
+ query = f"""SELECT thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata
+ FROM checkpoints
+ {where}
+ ORDER BY checkpoint_id DESC"""
+ if limit is not None:
+ query += " LIMIT ?"
+ param_values = (*param_values, limit)
+ with self.cursor(transaction=False) as cur, closing(self.conn.cursor()) as wcur:
+ cur.execute(query, param_values)
+ for (
+ thread_id,
+ checkpoint_ns,
+ checkpoint_id,
+ parent_checkpoint_id,
+ type,
+ checkpoint,
+ metadata,
+ ) in cur:
+ wcur.execute(
+ "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx",
+ (thread_id, checkpoint_ns, checkpoint_id),
+ )
+ yield CheckpointTuple(
+ {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": checkpoint_id,
+ }
+ },
+ self.serde.loads_typed((type, checkpoint)),
+ cast(
+ CheckpointMetadata,
+ json.loads(metadata) if metadata is not None else {},
+ ),
+ (
+ {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": parent_checkpoint_id,
+ }
+ }
+ if parent_checkpoint_id
+ else None
+ ),
+ [
+ (task_id, channel, self.serde.loads_typed((type, value)))
+ for task_id, channel, type, value in wcur
+ ],
+ )
+
+ def put(
+ self,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ """Save a checkpoint to the database.
+
+ This method saves a checkpoint to the SQLite database. The checkpoint is associated
+ with the provided config and its parent config (if any).
+
+ Args:
+ config: The config to associate with the checkpoint.
+ checkpoint: The checkpoint to save.
+ metadata: Additional metadata to save with the checkpoint.
+ new_versions: New channel versions as of this write.
+
+ Returns:
+ RunnableConfig: Updated configuration after storing the checkpoint.
+
+ Examples:
+
+ >>> from langgraph.checkpoint.sqlite import SqliteSaver
+ >>> with SqliteSaver.from_conn_string(":memory:") as memory:
+ >>> config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
+ >>> checkpoint = {"ts": "2024-05-04T06:32:42.235444+00:00", "id": "1ef4f797-8335-6428-8001-8a1503f9b875", "channel_values": {"key": "value"}}
+ >>> saved_config = memory.put(config, checkpoint, {"source": "input", "step": 1, "writes": {"key": "value"}}, {})
+ >>> print(saved_config)
+ {'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef4f797-8335-6428-8001-8a1503f9b875'}}
+ """
+ thread_id = config["configurable"]["thread_id"]
+ checkpoint_ns = config["configurable"]["checkpoint_ns"]
+ type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
+ serialized_metadata = json.dumps(
+ get_checkpoint_metadata(config, metadata), ensure_ascii=False
+ ).encode("utf-8", "ignore")
+ with self.cursor() as cur:
+ cur.execute(
+ "INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
+ (
+ str(config["configurable"]["thread_id"]),
+ checkpoint_ns,
+ checkpoint["id"],
+ config["configurable"].get("checkpoint_id"),
+ type_,
+ serialized_checkpoint,
+ serialized_metadata,
+ ),
+ )
+ return {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": checkpoint["id"],
+ }
+ }
+
+ def put_writes(
+ self,
+ config: RunnableConfig,
+ writes: Sequence[tuple[str, Any]],
+ task_id: str,
+ task_path: str = "",
+ ) -> None:
+ """Store intermediate writes linked to a checkpoint.
+
+ This method saves intermediate writes associated with a checkpoint to the SQLite database.
+
+ Args:
+ config: Configuration of the related checkpoint.
+ writes: List of writes to store, each as (channel, value) pair.
+ task_id: Identifier for the task creating the writes.
+ task_path: Path of the task creating the writes.
+ """
+ query = (
+ "INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
+ if all(w[0] in WRITES_IDX_MAP for w in writes)
+ else "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
+ )
+ with self.cursor() as cur:
+ cur.executemany(
+ query,
+ [
+ (
+ str(config["configurable"]["thread_id"]),
+ str(config["configurable"]["checkpoint_ns"]),
+ str(config["configurable"]["checkpoint_id"]),
+ task_id,
+ WRITES_IDX_MAP.get(channel, idx),
+ channel,
+ *self.serde.dumps_typed(value),
+ )
+ for idx, (channel, value) in enumerate(writes)
+ ],
+ )
+
+ def delete_thread(self, thread_id: str) -> None:
+ """Delete all checkpoints and writes associated with a thread ID.
+
+ Args:
+ thread_id: The thread ID to delete.
+
+ Returns:
+ None
+ """
+ with self.cursor() as cur:
+ cur.execute(
+ "DELETE FROM checkpoints WHERE thread_id = ?",
+ (str(thread_id),),
+ )
+ cur.execute(
+ "DELETE FROM writes WHERE thread_id = ?",
+ (str(thread_id),),
+ )
+
+ async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
+ """Get a checkpoint tuple from the database asynchronously.
+
+ Note:
+ This async method is not supported by the SqliteSaver class.
+ Use get_tuple() instead, or consider using [AsyncSqliteSaver][langgraph.checkpoint.sqlite.aio.AsyncSqliteSaver].
+ """
+ raise NotImplementedError(_AIO_ERROR_MSG)
+
+ async def alist(
+ self,
+ config: RunnableConfig | None,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> AsyncIterator[CheckpointTuple]:
+ """List checkpoints from the database asynchronously.
+
+ Note:
+ This async method is not supported by the SqliteSaver class.
+ Use list() instead, or consider using [AsyncSqliteSaver][langgraph.checkpoint.sqlite.aio.AsyncSqliteSaver].
+ """
+ raise NotImplementedError(_AIO_ERROR_MSG)
+ yield
+
+ async def aput(
+ self,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ """Save a checkpoint to the database asynchronously.
+
+ Note:
+ This async method is not supported by the SqliteSaver class.
+ Use put() instead, or consider using [AsyncSqliteSaver][langgraph.checkpoint.sqlite.aio.AsyncSqliteSaver].
+ """
+ raise NotImplementedError(_AIO_ERROR_MSG)
+
+ def get_next_version(self, current: str | None, channel: None) -> str:
+ """Generate the next version ID for a channel.
+
+ This method creates a new version identifier for a channel based on its current version.
+
+ Args:
+ current (Optional[str]): The current version identifier of the channel.
+
+ Returns:
+ str: The next version identifier, which is guaranteed to be monotonically increasing.
+ """
+ if current is None:
+ current_v = 0
+ elif isinstance(current, int):
+ current_v = current
+ else:
+ current_v = int(current.split(".")[0])
+ next_v = current_v + 1
+ next_h = random.random()
+ return f"{next_v:032}.{next_h:016}"
diff --git a/langgraph/source/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py b/langgraph/source/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py
new file mode 100644
index 0000000000000000000000000000000000000000..e20b11f4c16904a08b3672d6032f1eead616ae7a
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py
@@ -0,0 +1,631 @@
+from __future__ import annotations
+
+import asyncio
+import json
+import random
+import threading
+from collections.abc import AsyncIterator, Callable, Iterator, Sequence
+from contextlib import asynccontextmanager
+from typing import Any, TypeVar, cast
+
+import aiosqlite
+from langchain_core.runnables import RunnableConfig
+from langgraph.checkpoint.base import (
+ WRITES_IDX_MAP,
+ BaseCheckpointSaver,
+ ChannelVersions,
+ Checkpoint,
+ CheckpointMetadata,
+ CheckpointTuple,
+ SerializerProtocol,
+ get_checkpoint_id,
+ get_checkpoint_metadata,
+)
+from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
+
+from langgraph.checkpoint.sqlite.utils import search_where
+
+T = TypeVar("T", bound=Callable)
+
+
+class AsyncSqliteSaver(BaseCheckpointSaver[str]):
+ """An asynchronous checkpoint saver that stores checkpoints in a SQLite database.
+
+ This class provides an asynchronous interface for saving and retrieving checkpoints
+ using a SQLite database. It's designed for use in asynchronous environments and
+ offers better performance for I/O-bound operations compared to synchronous alternatives.
+
+ Attributes:
+ conn (aiosqlite.Connection): The asynchronous SQLite database connection.
+ serde (SerializerProtocol): The serializer used for encoding/decoding checkpoints.
+
+ Tip:
+ Requires the [aiosqlite](https://pypi.org/project/aiosqlite/) package.
+ Install it with `pip install aiosqlite`.
+
+ Warning:
+ While this class supports asynchronous checkpointing, it is not recommended
+ for production workloads due to limitations in SQLite's write performance.
+ For production use, consider a more robust database like PostgreSQL.
+
+ Tip:
+ Remember to **close the database connection** after executing your code,
+ otherwise, you may see the graph "hang" after execution (since the program
+ will not exit until the connection is closed).
+
+ The easiest way is to use the `async with` statement as shown in the examples.
+
+ ```python
+ async with AsyncSqliteSaver.from_conn_string("checkpoints.sqlite") as saver:
+ # Your code here
+ graph = builder.compile(checkpointer=saver)
+ config = {"configurable": {"thread_id": "thread-1"}}
+ async for event in graph.astream_events(..., config, version="v1"):
+ print(event)
+ ```
+
+ Examples:
+ Usage within StateGraph:
+
+ ```pycon
+ >>> import asyncio
+ >>>
+ >>> from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
+ >>> from langgraph.graph import StateGraph
+ >>>
+ >>> async def main():
+ >>> builder = StateGraph(int)
+ >>> builder.add_node("add_one", lambda x: x + 1)
+ >>> builder.set_entry_point("add_one")
+ >>> builder.set_finish_point("add_one")
+ >>> async with AsyncSqliteSaver.from_conn_string("checkpoints.db") as memory:
+ >>> graph = builder.compile(checkpointer=memory)
+ >>> coro = graph.ainvoke(1, {"configurable": {"thread_id": "thread-1"}})
+ >>> print(await asyncio.gather(coro))
+ >>>
+ >>> asyncio.run(main())
+ Output: [2]
+ ```
+ Raw usage:
+
+ ```pycon
+ >>> import asyncio
+ >>> import aiosqlite
+ >>> from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
+ >>>
+ >>> async def main():
+ >>> async with aiosqlite.connect("checkpoints.db") as conn:
+ ... saver = AsyncSqliteSaver(conn)
+ ... config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
+ ... checkpoint = {"ts": "2023-05-03T10:00:00Z", "data": {"key": "value"}, "id": "0c62ca34-ac19-445d-bbb0-5b4984975b2a"}
+ ... saved_config = await saver.aput(config, checkpoint, {}, {})
+ ... print(saved_config)
+ >>> asyncio.run(main())
+ {'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '0c62ca34-ac19-445d-bbb0-5b4984975b2a'}}
+ ```
+ """
+
+ lock: asyncio.Lock
+ is_setup: bool
+
+ def __init__(
+ self,
+ conn: aiosqlite.Connection,
+ *,
+ serde: SerializerProtocol | None = None,
+ ):
+ super().__init__(serde=serde)
+ self.jsonplus_serde = JsonPlusSerializer()
+ self.conn = conn
+ self.lock = asyncio.Lock()
+ self.loop = asyncio.get_running_loop()
+ self.is_setup = False
+
+ @classmethod
+ @asynccontextmanager
+ async def from_conn_string(
+ cls, conn_string: str
+ ) -> AsyncIterator[AsyncSqliteSaver]:
+ """Create a new AsyncSqliteSaver instance from a connection string.
+
+ Args:
+ conn_string: The SQLite connection string.
+
+ Yields:
+ AsyncSqliteSaver: A new AsyncSqliteSaver instance.
+ """
+ async with aiosqlite.connect(conn_string) as conn:
+ yield cls(conn)
+
+ def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
+ """Get a checkpoint tuple from the database.
+
+ This method retrieves a checkpoint tuple from the SQLite database based on the
+ provided config. If the config contains a `checkpoint_id` key, the checkpoint with
+ the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
+ for the given thread ID is retrieved.
+
+ Args:
+ config: The config to use for retrieving the checkpoint.
+
+ Returns:
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
+ """
+ try:
+ # check if we are in the main thread, only bg threads can block
+ # we don't check in other methods to avoid the overhead
+ if asyncio.get_running_loop() is self.loop:
+ raise asyncio.InvalidStateError(
+ "Synchronous calls to AsyncSqliteSaver are only allowed from a "
+ "different thread. From the main thread, use the async interface. "
+ "For example, use `await checkpointer.aget_tuple(...)` or `await "
+ "graph.ainvoke(...)`."
+ )
+ except RuntimeError:
+ pass
+ return asyncio.run_coroutine_threadsafe(
+ self.aget_tuple(config), self.loop
+ ).result()
+
+ def list(
+ self,
+ config: RunnableConfig | None,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> Iterator[CheckpointTuple]:
+ """List checkpoints from the database asynchronously.
+
+ This method retrieves a list of checkpoint tuples from the SQLite database based
+ on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
+
+ Args:
+ config: Base configuration for filtering checkpoints.
+ filter: Additional filtering criteria for metadata.
+ before: If provided, only checkpoints before the specified checkpoint ID are returned.
+ limit: Maximum number of checkpoints to return.
+
+ Yields:
+ An iterator of matching checkpoint tuples.
+ """
+ try:
+ # check if we are in the main thread, only bg threads can block
+ # we don't check in other methods to avoid the overhead
+ if asyncio.get_running_loop() is self.loop:
+ raise asyncio.InvalidStateError(
+ "Synchronous calls to AsyncSqliteSaver are only allowed from a "
+ "different thread. From the main thread, use the async interface. "
+ "For example, use `checkpointer.alist(...)` or `await "
+ "graph.ainvoke(...)`."
+ )
+ except RuntimeError:
+ pass
+ aiter_ = self.alist(config, filter=filter, before=before, limit=limit)
+ while True:
+ try:
+ yield asyncio.run_coroutine_threadsafe(
+ anext(aiter_), # type: ignore[arg-type] # noqa: F821
+ self.loop,
+ ).result()
+ except StopAsyncIteration:
+ break
+
+ def put(
+ self,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ """Save a checkpoint to the database.
+
+ This method saves a checkpoint to the SQLite database. The checkpoint is associated
+ with the provided config and its parent config (if any).
+
+ Args:
+ config: The config to associate with the checkpoint.
+ checkpoint: The checkpoint to save.
+ metadata: Additional metadata to save with the checkpoint.
+ new_versions: New channel versions as of this write.
+
+ Returns:
+ RunnableConfig: Updated configuration after storing the checkpoint.
+ """
+ return asyncio.run_coroutine_threadsafe(
+ self.aput(config, checkpoint, metadata, new_versions), self.loop
+ ).result()
+
+ def put_writes(
+ self,
+ config: RunnableConfig,
+ writes: Sequence[tuple[str, Any]],
+ task_id: str,
+ task_path: str = "",
+ ) -> None:
+ return asyncio.run_coroutine_threadsafe(
+ self.aput_writes(config, writes, task_id, task_path), self.loop
+ ).result()
+
+ def delete_thread(self, thread_id: str) -> None:
+ """Delete all checkpoints and writes associated with a thread ID.
+
+ Args:
+ thread_id: The thread ID to delete.
+
+ Returns:
+ None
+ """
+ try:
+ # check if we are in the main thread, only bg threads can block
+ # we don't check in other methods to avoid the overhead
+ if asyncio.get_running_loop() is self.loop:
+ raise asyncio.InvalidStateError(
+ "Synchronous calls to AsyncSqliteSaver are only allowed from a "
+ "different thread. From the main thread, use the async interface. "
+ "For example, use `checkpointer.alist(...)` or `await "
+ "graph.ainvoke(...)`."
+ )
+ except RuntimeError:
+ pass
+ return asyncio.run_coroutine_threadsafe(
+ self.adelete_thread(thread_id), self.loop
+ ).result()
+
+ async def setup(self) -> None:
+ """Set up the checkpoint database asynchronously.
+
+ This method creates the necessary tables in the SQLite database if they don't
+ already exist. It is called automatically when needed and should not be called
+ directly by the user.
+ """
+ async with self.lock:
+ if self.is_setup:
+ return
+ await _ensure_connected(self.conn)
+ async with self.conn.executescript(
+ """
+ PRAGMA journal_mode=WAL;
+ CREATE TABLE IF NOT EXISTS checkpoints (
+ thread_id TEXT NOT NULL,
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
+ checkpoint_id TEXT NOT NULL,
+ parent_checkpoint_id TEXT,
+ type TEXT,
+ checkpoint BLOB,
+ metadata BLOB,
+ PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
+ );
+ CREATE TABLE IF NOT EXISTS writes (
+ thread_id TEXT NOT NULL,
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
+ checkpoint_id TEXT NOT NULL,
+ task_id TEXT NOT NULL,
+ idx INTEGER NOT NULL,
+ channel TEXT NOT NULL,
+ type TEXT,
+ value BLOB,
+ PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
+ );
+ """
+ ):
+ await self.conn.commit()
+
+ self.is_setup = True
+
+ async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
+ """Get a checkpoint tuple from the database asynchronously.
+
+ This method retrieves a checkpoint tuple from the SQLite database based on the
+ provided config. If the config contains a `checkpoint_id` key, the checkpoint with
+ the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
+ for the given thread ID is retrieved.
+
+ Args:
+ config: The config to use for retrieving the checkpoint.
+
+ Returns:
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
+ """
+ await self.setup()
+ checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
+ async with self.lock, self.conn.cursor() as cur:
+ # find the latest checkpoint for the thread_id
+ if checkpoint_id := get_checkpoint_id(config):
+ await cur.execute(
+ "SELECT thread_id, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata FROM checkpoints WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?",
+ (
+ str(config["configurable"]["thread_id"]),
+ checkpoint_ns,
+ checkpoint_id,
+ ),
+ )
+ else:
+ await cur.execute(
+ "SELECT thread_id, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata FROM checkpoints WHERE thread_id = ? AND checkpoint_ns = ? ORDER BY checkpoint_id DESC LIMIT 1",
+ (str(config["configurable"]["thread_id"]), checkpoint_ns),
+ )
+ # if a checkpoint is found, return it
+ if value := await cur.fetchone():
+ (
+ thread_id,
+ checkpoint_id,
+ parent_checkpoint_id,
+ type,
+ checkpoint,
+ metadata,
+ ) = value
+ if not get_checkpoint_id(config):
+ config = {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": checkpoint_id,
+ }
+ }
+ # find any pending writes
+ await cur.execute(
+ "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx",
+ (
+ str(config["configurable"]["thread_id"]),
+ checkpoint_ns,
+ str(config["configurable"]["checkpoint_id"]),
+ ),
+ )
+ # deserialize the checkpoint and metadata
+ return CheckpointTuple(
+ config,
+ self.serde.loads_typed((type, checkpoint)),
+ cast(
+ CheckpointMetadata,
+ (json.loads(metadata) if metadata is not None else {}),
+ ),
+ (
+ {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": parent_checkpoint_id,
+ }
+ }
+ if parent_checkpoint_id
+ else None
+ ),
+ [
+ (task_id, channel, self.serde.loads_typed((type, value)))
+ async for task_id, channel, type, value in cur
+ ],
+ )
+
+ async def alist(
+ self,
+ config: RunnableConfig | None,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> AsyncIterator[CheckpointTuple]:
+ """List checkpoints from the database asynchronously.
+
+ This method retrieves a list of checkpoint tuples from the SQLite database based
+ on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
+
+ Args:
+ config: Base configuration for filtering checkpoints.
+ filter: Additional filtering criteria for metadata.
+ before: If provided, only checkpoints before the specified checkpoint ID are returned.
+ limit: Maximum number of checkpoints to return.
+
+ Yields:
+ An asynchronous iterator of matching checkpoint tuples.
+ """
+ await self.setup()
+ where, params = search_where(config, filter, before)
+ query = f"""SELECT thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata
+ FROM checkpoints
+ {where}
+ ORDER BY checkpoint_id DESC"""
+ if limit is not None:
+ query += " LIMIT ?"
+ params = (*params, limit)
+ async with (
+ self.lock,
+ self.conn.execute(query, params) as cur,
+ self.conn.cursor() as wcur,
+ ):
+ async for (
+ thread_id,
+ checkpoint_ns,
+ checkpoint_id,
+ parent_checkpoint_id,
+ type,
+ checkpoint,
+ metadata,
+ ) in cur:
+ await wcur.execute(
+ "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx",
+ (thread_id, checkpoint_ns, checkpoint_id),
+ )
+ yield CheckpointTuple(
+ {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": checkpoint_id,
+ }
+ },
+ self.serde.loads_typed((type, checkpoint)),
+ cast(
+ CheckpointMetadata,
+ (json.loads(metadata) if metadata is not None else {}),
+ ),
+ (
+ {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": parent_checkpoint_id,
+ }
+ }
+ if parent_checkpoint_id
+ else None
+ ),
+ [
+ (task_id, channel, self.serde.loads_typed((type, value)))
+ async for task_id, channel, type, value in wcur
+ ],
+ )
+
+ async def aput(
+ self,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ """Save a checkpoint to the database asynchronously.
+
+ This method saves a checkpoint to the SQLite database. The checkpoint is associated
+ with the provided config and its parent config (if any).
+
+ Args:
+ config: The config to associate with the checkpoint.
+ checkpoint: The checkpoint to save.
+ metadata: Additional metadata to save with the checkpoint.
+ new_versions: New channel versions as of this write.
+
+ Returns:
+ RunnableConfig: Updated configuration after storing the checkpoint.
+ """
+ await self.setup()
+ thread_id = config["configurable"]["thread_id"]
+ checkpoint_ns = config["configurable"]["checkpoint_ns"]
+ type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
+ serialized_metadata = json.dumps(
+ get_checkpoint_metadata(config, metadata), ensure_ascii=False
+ ).encode("utf-8", "ignore")
+ async with (
+ self.lock,
+ self.conn.execute(
+ "INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
+ (
+ str(config["configurable"]["thread_id"]),
+ checkpoint_ns,
+ checkpoint["id"],
+ config["configurable"].get("checkpoint_id"),
+ type_,
+ serialized_checkpoint,
+ serialized_metadata,
+ ),
+ ),
+ ):
+ await self.conn.commit()
+ return {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": checkpoint["id"],
+ }
+ }
+
+ async def aput_writes(
+ self,
+ config: RunnableConfig,
+ writes: Sequence[tuple[str, Any]],
+ task_id: str,
+ task_path: str = "",
+ ) -> None:
+ """Store intermediate writes linked to a checkpoint asynchronously.
+
+ This method saves intermediate writes associated with a checkpoint to the database.
+
+ Args:
+ config: Configuration of the related checkpoint.
+ writes: List of writes to store, each as (channel, value) pair.
+ task_id: Identifier for the task creating the writes.
+ task_path: Path of the task creating the writes.
+ """
+ query = (
+ "INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
+ if all(w[0] in WRITES_IDX_MAP for w in writes)
+ else "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
+ )
+ await self.setup()
+ async with self.lock, self.conn.cursor() as cur:
+ await cur.executemany(
+ query,
+ [
+ (
+ str(config["configurable"]["thread_id"]),
+ str(config["configurable"]["checkpoint_ns"]),
+ str(config["configurable"]["checkpoint_id"]),
+ task_id,
+ WRITES_IDX_MAP.get(channel, idx),
+ channel,
+ *self.serde.dumps_typed(value),
+ )
+ for idx, (channel, value) in enumerate(writes)
+ ],
+ )
+ await self.conn.commit()
+
+ async def adelete_thread(self, thread_id: str) -> None:
+ """Delete all checkpoints and writes associated with a thread ID.
+
+ Args:
+ thread_id: The thread ID to delete.
+
+ Returns:
+ None
+ """
+ async with self.lock, self.conn.cursor() as cur:
+ await cur.execute(
+ "DELETE FROM checkpoints WHERE thread_id = ?",
+ (str(thread_id),),
+ )
+ await cur.execute(
+ "DELETE FROM writes WHERE thread_id = ?",
+ (str(thread_id),),
+ )
+ await self.conn.commit()
+
+ def get_next_version(self, current: str | None, channel: None) -> str:
+ """Generate the next version ID for a channel.
+
+ This method creates a new version identifier for a channel based on its current version.
+
+ Args:
+ current (Optional[str]): The current version identifier of the channel.
+
+ Returns:
+ str: The next version identifier, which is guaranteed to be monotonically increasing.
+ """
+ if current is None:
+ current_v = 0
+ elif isinstance(current, int):
+ current_v = current
+ else:
+ current_v = int(current.split(".")[0])
+ next_v = current_v + 1
+ next_h = random.random()
+ return f"{next_v:032}.{next_h:016}"
+
+
+async def _ensure_connected(conn: aiosqlite.Connection) -> None:
+ if not _CONN_STARTED_CHECK(conn):
+ await conn
+
+
+def _build_conn_started_check() -> Callable[[aiosqlite.Connection], bool]:
+ is_alive = getattr(aiosqlite.Connection, "is_alive", None)
+ if callable(is_alive):
+ return lambda conn: conn.is_alive() # type: ignore[attr-defined]
+
+ def _started(conn: aiosqlite.Connection) -> bool:
+ thread: threading.Thread | None = getattr(conn, "_thread", None)
+ return False if thread is None else thread.is_alive()
+
+ return _started
+
+
+_CONN_STARTED_CHECK = _build_conn_started_check()
diff --git a/langgraph/source/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/py.typed b/langgraph/source/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/py.typed
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py b/langgraph/source/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c7e0600053ebb73eafa36d2e617c76eab547475
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py
@@ -0,0 +1,116 @@
+from __future__ import annotations
+
+import json
+import re
+from collections.abc import Sequence
+from typing import Any
+
+from langchain_core.runnables import RunnableConfig
+from langgraph.checkpoint.base import get_checkpoint_id
+
+_FILTER_PATTERN = re.compile(r"^[a-zA-Z0-9_.-]+$")
+
+
+def _validate_filter_key(key: str) -> None:
+ """Validate that a filter key is safe for use in SQL queries.
+
+ Args:
+ key: The filter key to validate
+
+ Raises:
+ ValueError: If the key contains invalid characters that could enable SQL injection
+ """
+ # Allow alphanumeric characters, underscores, dots, and hyphens
+ # This covers typical JSON property names while preventing SQL injection
+ if not _FILTER_PATTERN.match(key):
+ raise ValueError(
+ f"Invalid filter key: '{key}'. Filter keys must contain only alphanumeric characters, underscores, dots, and hyphens."
+ )
+
+
+def _metadata_predicate(
+ metadata_filter: dict[str, Any],
+) -> tuple[Sequence[str], Sequence[Any]]:
+ """Return WHERE clause predicates for (a)search() given metadata filter.
+
+ This method returns a tuple of a string and a tuple of values. The string
+ is the parametered WHERE clause predicate (excluding the WHERE keyword):
+ "column1 = ? AND column2 IS ?". The tuple of values contains the values
+ for each of the corresponding parameters.
+ """
+
+ def _where_value(query_value: Any) -> tuple[str, Any]:
+ """Return tuple of operator and value for WHERE clause predicate."""
+ if query_value is None:
+ return ("IS ?", None)
+ elif (
+ isinstance(query_value, str)
+ or isinstance(query_value, int)
+ or isinstance(query_value, float)
+ ):
+ return ("= ?", query_value)
+ elif isinstance(query_value, bool):
+ return ("= ?", 1 if query_value else 0)
+ elif isinstance(query_value, dict) or isinstance(query_value, list):
+ # query value for JSON object cannot have trailing space after separators (, :)
+ # SQLite json_extract() returns JSON string without whitespace
+ return ("= ?", json.dumps(query_value, separators=(",", ":")))
+ else:
+ return ("= ?", str(query_value))
+
+ predicates = []
+ param_values = []
+
+ # process metadata query
+ for query_key, query_value in metadata_filter.items():
+ _validate_filter_key(query_key)
+ operator, param_value = _where_value(query_value)
+ predicates.append(
+ f"json_extract(CAST(metadata AS TEXT), '$.{query_key}') {operator}"
+ )
+ param_values.append(param_value)
+
+ return (predicates, param_values)
+
+
+def search_where(
+ config: RunnableConfig | None,
+ filter: dict[str, Any] | None,
+ before: RunnableConfig | None = None,
+) -> tuple[str, Sequence[Any]]:
+ """Return WHERE clause predicates for (a)search() given metadata filter
+ and `before` config.
+
+ This method returns a tuple of a string and a tuple of values. The string
+ is the parametered WHERE clause predicate (including the WHERE keyword):
+ "WHERE column1 = ? AND column2 IS ?". The tuple of values contains the
+ values for each of the corresponding parameters.
+ """
+ wheres = []
+ param_values = []
+
+ # construct predicate for config filter
+ if config is not None:
+ wheres.append("thread_id = ?")
+ param_values.append(config["configurable"]["thread_id"])
+ checkpoint_ns = config["configurable"].get("checkpoint_ns")
+ if checkpoint_ns is not None:
+ wheres.append("checkpoint_ns = ?")
+ param_values.append(checkpoint_ns)
+
+ if checkpoint_id := get_checkpoint_id(config):
+ wheres.append("checkpoint_id = ?")
+ param_values.append(checkpoint_id)
+
+ # construct predicate for metadata filter
+ if filter:
+ metadata_predicates, metadata_values = _metadata_predicate(filter)
+ wheres.extend(metadata_predicates)
+ param_values.extend(metadata_values)
+
+ # construct predicate for `before`
+ if before is not None:
+ wheres.append("checkpoint_id < ?")
+ param_values.append(get_checkpoint_id(before))
+
+ return ("WHERE " + " AND ".join(wheres) if wheres else "", param_values)
diff --git a/langgraph/source/libs/checkpoint-sqlite/langgraph/store/__init__.py b/langgraph/source/libs/checkpoint-sqlite/langgraph/store/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/langgraph/store/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/checkpoint-sqlite/langgraph/store/sqlite/__init__.py b/langgraph/source/libs/checkpoint-sqlite/langgraph/store/sqlite/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..b70192f5ee50b9589cb7c8ccdffa0bd9c49df0e5
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/langgraph/store/sqlite/__init__.py
@@ -0,0 +1,4 @@
+from langgraph.store.sqlite.aio import AsyncSqliteStore
+from langgraph.store.sqlite.base import SqliteStore
+
+__all__ = ["AsyncSqliteStore", "SqliteStore"]
diff --git a/langgraph/source/libs/checkpoint-sqlite/langgraph/store/sqlite/aio.py b/langgraph/source/libs/checkpoint-sqlite/langgraph/store/sqlite/aio.py
new file mode 100644
index 0000000000000000000000000000000000000000..f4594d55ea189a41f91c750bd64330fed8662f62
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/langgraph/store/sqlite/aio.py
@@ -0,0 +1,623 @@
+from __future__ import annotations
+
+import asyncio
+import logging
+from collections import defaultdict
+from collections.abc import AsyncIterator, Callable, Iterable, Sequence
+from contextlib import asynccontextmanager
+from types import TracebackType
+from typing import Any, cast
+
+import aiosqlite
+import orjson
+import sqlite_vec # type: ignore[import-untyped]
+from langgraph.store.base import (
+ GetOp,
+ ListNamespacesOp,
+ Op,
+ PutOp,
+ Result,
+ SearchOp,
+ TTLConfig,
+)
+from langgraph.store.base.batch import AsyncBatchedBaseStore
+
+from langgraph.store.sqlite.base import (
+ _PLACEHOLDER,
+ BaseSqliteStore,
+ SqliteIndexConfig,
+ _decode_ns_text,
+ _ensure_index_config,
+ _group_ops,
+ _row_to_item,
+ _row_to_search_item,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
+ """Asynchronous SQLite-backed store with optional vector search.
+
+ This class provides an asynchronous interface for storing and retrieving data
+ using a SQLite database with support for vector search capabilities.
+
+ Examples:
+ Basic setup and usage:
+ ```python
+ from langgraph.store.sqlite import AsyncSqliteStore
+
+ async with AsyncSqliteStore.from_conn_string(":memory:") as store:
+ await store.setup() # Run migrations
+
+ # 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_openai import OpenAIEmbeddings
+ from langgraph.store.sqlite import AsyncSqliteStore
+
+ async with AsyncSqliteStore.from_conn_string(
+ ":memory:",
+ index={
+ "dims": 1536,
+ "embed": OpenAIEmbeddings(),
+ "fields": ["text"] # specify which fields to embed
+ }
+ ) as store:
+ await store.setup() # Run migrations once
+
+ # Store documents
+ await store.aput(("docs",), "doc1", {"text": "Python tutorial"})
+ await store.aput(("docs",), "doc2", {"text": "TypeScript guide"})
+ await store.aput(("docs",), "doc3", {"text": "Other guide"}, index=False) # don't index
+
+ # Search by similarity
+ results = await store.asearch(("docs",), query="programming guides", limit=2)
+ ```
+
+ Warning:
+ Make sure to call `setup()` before first use to create necessary tables and indexes.
+
+ Note:
+ This class requires the aiosqlite package. Install with `pip install aiosqlite`.
+ """
+
+ def __init__(
+ self,
+ conn: aiosqlite.Connection,
+ *,
+ deserializer: Callable[[bytes | str | orjson.Fragment], dict[str, Any]]
+ | None = None,
+ index: SqliteIndexConfig | None = None,
+ ttl: TTLConfig | None = None,
+ ):
+ """Initialize the async SQLite store.
+
+ Args:
+ conn: The SQLite database connection.
+ deserializer: Optional custom deserializer function for values.
+ index: Optional vector search configuration.
+ ttl: Optional time-to-live configuration.
+ """
+ super().__init__()
+ self._deserializer = deserializer
+ self.conn = conn
+ self.lock = asyncio.Lock()
+ self.loop = asyncio.get_running_loop()
+ self.is_setup = False
+ self.index_config = index
+ if self.index_config:
+ self.embeddings, self.index_config = _ensure_index_config(self.index_config)
+ else:
+ self.embeddings = None
+ self.ttl_config = ttl
+ self._ttl_sweeper_task: asyncio.Task[None] | None = None
+ self._ttl_stop_event = asyncio.Event()
+
+ @classmethod
+ @asynccontextmanager
+ async def from_conn_string(
+ cls,
+ conn_string: str,
+ *,
+ index: SqliteIndexConfig | None = None,
+ ttl: TTLConfig | None = None,
+ ) -> AsyncIterator[AsyncSqliteStore]:
+ """Create a new AsyncSqliteStore instance from a connection string.
+
+ Args:
+ conn_string: The SQLite connection string.
+ index: Optional vector search configuration.
+ ttl: Optional time-to-live configuration.
+
+ Returns:
+ An AsyncSqliteStore instance wrapped in an async context manager.
+ """
+ async with aiosqlite.connect(conn_string, isolation_level=None) as conn:
+ yield cls(conn, index=index, ttl=ttl)
+
+ async def setup(self) -> None:
+ """Set up the store database.
+
+ This method creates the necessary tables in the SQLite database if they don't
+ already exist and runs database migrations. It should be called before first use.
+ """
+ async with self.lock:
+ if self.is_setup:
+ return
+
+ # Create migrations table if it doesn't exist
+ await self.conn.execute(
+ """
+ CREATE TABLE IF NOT EXISTS store_migrations (
+ v INTEGER PRIMARY KEY
+ )
+ """
+ )
+
+ # Check current migration version
+ async with self.conn.execute(
+ "SELECT v FROM store_migrations ORDER BY v DESC LIMIT 1"
+ ) as cur:
+ row = await cur.fetchone()
+ if row is None:
+ version = -1
+ else:
+ version = row[0]
+
+ # Apply migrations
+ for v, sql in enumerate(self.MIGRATIONS[version + 1 :], start=version + 1):
+ await self.conn.executescript(sql)
+ await self.conn.execute(
+ "INSERT INTO store_migrations (v) VALUES (?)", (v,)
+ )
+
+ # Apply vector migrations if index config is provided
+ if self.index_config:
+ # Create vector migrations table if it doesn't exist
+ await self.conn.enable_load_extension(True)
+ await self.conn.load_extension(sqlite_vec.loadable_path())
+ await self.conn.enable_load_extension(False)
+ await self.conn.execute(
+ """
+ CREATE TABLE IF NOT EXISTS vector_migrations (
+ v INTEGER PRIMARY KEY
+ )
+ """
+ )
+
+ # Check current vector migration version
+ async with self.conn.execute(
+ "SELECT v FROM vector_migrations ORDER BY v DESC LIMIT 1"
+ ) as cur:
+ row = await cur.fetchone()
+ if row is None:
+ version = -1
+ else:
+ version = row[0]
+
+ # Apply vector migrations
+ for v, sql in enumerate(
+ self.VECTOR_MIGRATIONS[version + 1 :], start=version + 1
+ ):
+ await self.conn.executescript(sql)
+ await self.conn.execute(
+ "INSERT INTO vector_migrations (v) VALUES (?)", (v,)
+ )
+
+ self.is_setup = True
+
+ @asynccontextmanager
+ async def _cursor(
+ self, *, transaction: bool = True
+ ) -> AsyncIterator[aiosqlite.Cursor]:
+ """Get a cursor for the SQLite database.
+
+ Args:
+ transaction: Whether to use a transaction for database operations.
+
+ Yields:
+ An SQLite cursor object.
+ """
+ if not self.is_setup:
+ await self.setup()
+ async with self.lock:
+ if transaction:
+ await self.conn.execute("BEGIN")
+
+ async with self.conn.cursor() as cur:
+ try:
+ yield cur
+ finally:
+ if transaction:
+ await self.conn.execute("COMMIT")
+
+ async def sweep_ttl(self) -> int:
+ """Delete expired store items based on TTL.
+
+ Returns:
+ int: The number of deleted items.
+ """
+ async with self._cursor() as cur:
+ await cur.execute(
+ """
+ DELETE FROM store
+ WHERE expires_at IS NOT NULL AND expires_at < CURRENT_TIMESTAMP
+ """
+ )
+ deleted_count = cur.rowcount
+ return deleted_count
+
+ async def start_ttl_sweeper(
+ self, sweep_interval_minutes: int | None = None
+ ) -> asyncio.Task[None]:
+ """Periodically delete expired store items based on TTL.
+
+ Returns:
+ Task that can be awaited or cancelled.
+ """
+ if not self.ttl_config:
+ return asyncio.create_task(asyncio.sleep(0))
+
+ if self._ttl_sweeper_task is not None and not self._ttl_sweeper_task.done():
+ return self._ttl_sweeper_task
+
+ self._ttl_stop_event.clear()
+
+ interval = float(
+ sweep_interval_minutes or self.ttl_config.get("sweep_interval_minutes") or 5
+ )
+ logger.info(f"Starting store TTL sweeper with interval {interval} minutes")
+
+ async def _sweep_loop() -> None:
+ while not self._ttl_stop_event.is_set():
+ try:
+ try:
+ await asyncio.wait_for(
+ self._ttl_stop_event.wait(),
+ timeout=interval * 60,
+ )
+ break
+ except asyncio.TimeoutError:
+ pass
+
+ expired_items = await self.sweep_ttl()
+ if expired_items > 0:
+ logger.info(f"Store swept {expired_items} expired items")
+ except asyncio.CancelledError:
+ break
+ except Exception as exc:
+ logger.exception("Store TTL sweep iteration failed", exc_info=exc)
+
+ task = asyncio.create_task(_sweep_loop())
+ task.set_name("ttl_sweeper")
+ self._ttl_sweeper_task = task
+ return task
+
+ async def stop_ttl_sweeper(self, timeout: float | None = None) -> bool:
+ """Stop the TTL sweeper task if it's running.
+
+ Args:
+ timeout: Maximum time to wait for the task to stop, in seconds.
+ If `None`, wait indefinitely.
+
+ Returns:
+ bool: True if the task was successfully stopped or wasn't running,
+ False if the timeout was reached before the task stopped.
+ """
+ if self._ttl_sweeper_task is None or self._ttl_sweeper_task.done():
+ return True
+
+ logger.info("Stopping TTL sweeper task")
+ self._ttl_stop_event.set()
+
+ if timeout is not None:
+ try:
+ await asyncio.wait_for(self._ttl_sweeper_task, timeout=timeout)
+ success = True
+ except asyncio.TimeoutError:
+ success = False
+ else:
+ await self._ttl_sweeper_task
+ success = True
+
+ if success:
+ self._ttl_sweeper_task = None
+ logger.info("TTL sweeper task stopped")
+ else:
+ logger.warning("Timed out waiting for TTL sweeper task to stop")
+
+ return success
+
+ async def __aenter__(self) -> AsyncSqliteStore:
+ return self
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ # Ensure the TTL sweeper task is stopped when exiting the context
+ if hasattr(self, "_ttl_sweeper_task") and self._ttl_sweeper_task is not None:
+ # Set the event to signal the task to stop
+ self._ttl_stop_event.set()
+ # We don't wait for the task to complete here to avoid blocking
+ # The task will clean up itself gracefully
+
+ async def abatch(self, ops: Iterable[Op]) -> list[Result]:
+ """Execute a batch of operations asynchronously.
+
+ Args:
+ ops: Iterable of operations to execute.
+
+ Returns:
+ List of operation results.
+ """
+ grouped_ops, num_ops = _group_ops(ops)
+ results: list[Result] = [None] * num_ops
+
+ async with self._cursor(transaction=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
+ )
+
+ return results
+
+ async def _batch_get_ops(
+ self,
+ get_ops: Sequence[tuple[int, GetOp]],
+ results: list[Result],
+ cur: aiosqlite.Cursor,
+ ) -> None:
+ """Process batch GET operations.
+
+ Args:
+ get_ops: Sequence of GET operations.
+ results: List to store results in.
+ cur: Database cursor.
+ """
+ # Group all queries by namespace to execute all operations for each namespace together
+ namespace_queries = defaultdict(list)
+ for prepared_query in self._get_batch_GET_ops_queries(get_ops):
+ namespace_queries[prepared_query.namespace].append(prepared_query)
+
+ # Process each namespace's operations
+ for namespace, queries in namespace_queries.items():
+ # Execute TTL refresh queries first
+ for query in queries:
+ if query.kind == "refresh":
+ try:
+ await cur.execute(query.query, query.params)
+ except Exception as e:
+ raise ValueError(
+ f"Error executing TTL refresh: \n{query.query}\n{query.params}\n{e}"
+ ) from e
+
+ # Then execute GET queries and process results
+ for query in queries:
+ if query.kind == "get":
+ try:
+ await cur.execute(query.query, query.params)
+ except Exception as e:
+ raise ValueError(
+ f"Error executing GET query: \n{query.query}\n{query.params}\n{e}"
+ ) from e
+
+ rows = await cur.fetchall()
+ key_to_row = {
+ row[0]: {
+ "key": row[0],
+ "value": row[1],
+ "created_at": row[2],
+ "updated_at": row[3],
+ "expires_at": row[4] if len(row) > 4 else None,
+ "ttl_minutes": row[5] if len(row) > 5 else None,
+ }
+ for row in rows
+ }
+
+ # Process results for this query
+ for idx, key in query.items:
+ row = key_to_row.get(key)
+ if row:
+ results[idx] = _row_to_item(
+ namespace, row, loader=self._deserializer
+ )
+ else:
+ results[idx] = None
+
+ async def _batch_put_ops(
+ self,
+ put_ops: Sequence[tuple[int, PutOp]],
+ cur: aiosqlite.Cursor,
+ ) -> None:
+ """Process batch PUT operations.
+
+ Args:
+ put_ops: Sequence of PUT operations.
+ cur: Database cursor.
+ """
+ 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 = await self.embeddings.aembed_documents(
+ [param[-1] for param in txt_params]
+ )
+
+ # Convert vectors to SQLite-friendly format
+ vector_params = []
+ for (ns, k, pathname, _), vector in zip(txt_params, vectors, strict=False):
+ vector_params.extend(
+ [ns, k, pathname, sqlite_vec.serialize_float32(vector)]
+ )
+
+ queries.append((query, vector_params))
+
+ for query, params in queries:
+ await cur.execute(query, params)
+
+ async def _batch_search_ops(
+ self,
+ search_ops: Sequence[tuple[int, SearchOp]],
+ results: list[Result],
+ cur: aiosqlite.Cursor,
+ ) -> None:
+ """Process batch SEARCH operations.
+
+ Args:
+ search_ops: Sequence of SEARCH operations.
+ results: List to store results in.
+ cur: Database cursor.
+ """
+ prepared_queries, embedding_requests = self._prepare_batch_search_queries(
+ search_ops
+ )
+
+ # Setup dot_product function if it doesn't exist
+ if embedding_requests and self.embeddings:
+ vectors = await self.embeddings.aembed_documents(
+ [query for _, query in embedding_requests]
+ )
+
+ for (embed_req_idx, _), embedding in zip(
+ embedding_requests, vectors, strict=False
+ ):
+ # Find the corresponding query in prepared_queries
+ # The embed_req_idx is the original index in search_ops, which should map to prepared_queries
+ if embed_req_idx < len(prepared_queries):
+ _params_list: list = prepared_queries[embed_req_idx][1]
+ for i, param in enumerate(_params_list):
+ if param is _PLACEHOLDER:
+ _params_list[i] = sqlite_vec.serialize_float32(embedding)
+ else:
+ logger.warning(
+ f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
+ )
+
+ for (original_op_idx, _), (query, params, needs_refresh) in zip(
+ search_ops, prepared_queries, strict=False
+ ):
+ await cur.execute(query, params)
+ rows = await cur.fetchall()
+
+ if needs_refresh and rows and self.ttl_config:
+ keys_to_refresh = []
+ for row_data in rows:
+ # Assuming row_data[0] is prefix (text), row_data[1] is key (text)
+ # These are raw text values directly from the DB.
+ keys_to_refresh.append((row_data[0], row_data[1]))
+
+ if keys_to_refresh:
+ updates_by_prefix = defaultdict(list)
+ for prefix_text, key_text in keys_to_refresh:
+ updates_by_prefix[prefix_text].append(key_text)
+
+ for prefix_text, key_list in updates_by_prefix.items():
+ placeholders = ",".join(["?"] * len(key_list))
+ update_query = f"""
+ UPDATE store
+ SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
+ WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
+ """
+ update_params = (prefix_text, *key_list)
+ try:
+ await cur.execute(update_query, update_params)
+ except Exception as e:
+ logger.error(
+ f"Error during TTL refresh update for search: {e}"
+ )
+
+ # Process rows into items
+ if "score" in query: # Vector search query
+ items = [
+ _row_to_search_item(
+ _decode_ns_text(row[0]), # prefix
+ {
+ "key": row[1], # key
+ "value": row[2], # value
+ "created_at": row[3],
+ "updated_at": row[4],
+ "expires_at": row[5] if len(row) > 5 else None,
+ "ttl_minutes": row[6] if len(row) > 6 else None,
+ "score": row[7] if len(row) > 7 else None,
+ },
+ loader=self._deserializer,
+ )
+ for row in rows
+ ]
+ else: # Regular search query
+ items = [
+ _row_to_search_item(
+ _decode_ns_text(row[0]), # prefix
+ {
+ "key": row[1], # key
+ "value": row[2], # value
+ "created_at": row[3],
+ "updated_at": row[4],
+ "expires_at": row[5] if len(row) > 5 else None,
+ "ttl_minutes": row[6] if len(row) > 6 else None,
+ },
+ loader=self._deserializer,
+ )
+ for row in rows
+ ]
+
+ results[original_op_idx] = items
+
+ async def _batch_list_namespaces_ops(
+ self,
+ list_ops: Sequence[tuple[int, ListNamespacesOp]],
+ results: list[Result],
+ cur: aiosqlite.Cursor,
+ ) -> None:
+ """Process batch LIST NAMESPACES operations.
+
+ Args:
+ list_ops: Sequence of LIST NAMESPACES operations.
+ results: List to store results in.
+ cur: Database cursor.
+ """
+ queries = self._get_batch_list_namespaces_queries(list_ops)
+ for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
+ await cur.execute(query, params)
+
+ rows = await cur.fetchall()
+ results[idx] = [_decode_ns_text(row[0]) for row in rows]
diff --git a/langgraph/source/libs/checkpoint-sqlite/langgraph/store/sqlite/base.py b/langgraph/source/libs/checkpoint-sqlite/langgraph/store/sqlite/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..ab167b1e6457663c0d87aaba9c710c2815164fc6
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/langgraph/store/sqlite/base.py
@@ -0,0 +1,1431 @@
+from __future__ import annotations
+
+import concurrent.futures
+import datetime
+import logging
+import re
+import sqlite3
+import threading
+from collections import defaultdict
+from collections.abc import Callable, Iterable, Iterator, Sequence
+from contextlib import contextmanager
+from typing import Any, Literal, NamedTuple, cast
+
+import orjson
+import sqlite_vec # type: ignore[import-untyped]
+from langgraph.store.base import (
+ BaseStore,
+ GetOp,
+ IndexConfig,
+ Item,
+ ListNamespacesOp,
+ Op,
+ PutOp,
+ Result,
+ SearchItem,
+ SearchOp,
+ TTLConfig,
+ ensure_embeddings,
+ get_text_at_path,
+ tokenize_path,
+)
+
+_AIO_ERROR_MSG = (
+ "The SqliteStore does not support async methods. "
+ "Consider using AsyncSqliteStore instead.\n"
+ "from langgraph.store.sqlite.aio import AsyncSqliteStore\n"
+)
+
+logger = logging.getLogger(__name__)
+
+MIGRATIONS = [
+ """
+CREATE TABLE IF NOT EXISTS store (
+ -- 'prefix' represents the doc's 'namespace'
+ prefix text NOT NULL,
+ key text NOT NULL,
+ value text NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (prefix, key)
+);
+""",
+ """
+-- For faster lookups by prefix
+CREATE INDEX IF NOT EXISTS store_prefix_idx ON store (prefix);
+""",
+ """
+-- Add expires_at column to store table
+ALTER TABLE store
+ADD COLUMN expires_at TIMESTAMP;
+""",
+ """
+-- Add ttl_minutes column to store table
+ALTER TABLE store
+ADD COLUMN ttl_minutes REAL;
+""",
+ """
+-- Add index for efficient TTL sweeping
+CREATE INDEX IF NOT EXISTS idx_store_expires_at ON store (expires_at)
+WHERE expires_at IS NOT NULL;
+""",
+]
+
+VECTOR_MIGRATIONS = [
+ """
+CREATE TABLE IF NOT EXISTS store_vectors (
+ prefix text NOT NULL,
+ key text NOT NULL,
+ field_name text NOT NULL,
+ embedding BLOB,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (prefix, key, field_name),
+ FOREIGN KEY (prefix, key) REFERENCES store(prefix, key) ON DELETE CASCADE
+);
+""",
+]
+
+
+class SqliteIndexConfig(IndexConfig):
+ """Configuration for vector embeddings in SQLite store."""
+
+ pass
+
+
+def _namespace_to_text(
+ namespace: tuple[str, ...], handle_wildcards: bool = False
+) -> str:
+ """Convert namespace tuple to text string."""
+ if handle_wildcards:
+ namespace = tuple("%" if val == "*" else val for val in namespace)
+ return ".".join(namespace)
+
+
+def _decode_ns_text(namespace: str) -> tuple[str, ...]:
+ """Convert namespace string to tuple."""
+ return tuple(namespace.split("."))
+
+
+_FILTER_PATTERN = re.compile(r"^[a-zA-Z0-9_.-]+$")
+
+
+def _validate_filter_key(key: str) -> None:
+ """Validate that a filter key is safe for use in SQL queries.
+
+ Args:
+ key: The filter key to validate
+
+ Raises:
+ ValueError: If the key contains invalid characters that could enable SQL injection
+ """
+ # Allow alphanumeric characters, underscores, dots, and hyphens
+ # This covers typical JSON property names while preventing SQL injection
+ if not _FILTER_PATTERN.match(key):
+ raise ValueError(
+ f"Invalid filter key: '{key}'. Filter keys must contain only alphanumeric characters, underscores, dots, and hyphens."
+ )
+
+
+def _json_loads(content: bytes | str | orjson.Fragment) -> Any:
+ if isinstance(content, orjson.Fragment):
+ if hasattr(content, "buf"):
+ content = content.buf
+ else:
+ if isinstance(content.contents, bytes):
+ content = content.contents
+ else:
+ content = content.contents.encode()
+ return orjson.loads(cast(bytes, content))
+ elif isinstance(content, bytes):
+ return orjson.loads(content)
+ else:
+ return orjson.loads(content)
+
+
+def _row_to_item(
+ namespace: tuple[str, ...],
+ row: dict[str, Any],
+ *,
+ loader: Callable[[bytes | str | orjson.Fragment], dict[str, Any]] | None = None,
+) -> Item:
+ """Convert a row from the database into an Item."""
+ val = row["value"]
+ 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(
+ namespace: tuple[str, ...],
+ row: dict[str, Any],
+ *,
+ loader: Callable[[bytes | str | orjson.Fragment], dict[str, Any]] | None = None,
+) -> SearchItem:
+ """Convert a row from the database into a SearchItem."""
+ loader = loader or _json_loads
+ val = row["value"]
+ score = row.get("score")
+ if score is not None:
+ try:
+ score = float(score)
+ 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
+ for idx, op in enumerate(ops):
+ grouped_ops[type(op)].append((idx, op))
+ tot += 1
+ return grouped_ops, tot
+
+
+class PreparedGetQuery(NamedTuple):
+ query: str # Main query to execute
+ params: tuple # Parameters for the main query
+ namespace: tuple[str, ...] # Namespace info
+ items: list # List of items this query is for
+ kind: Literal["get", "refresh"]
+
+
+class BaseSqliteStore:
+ """Shared base class for SQLite stores."""
+
+ MIGRATIONS = MIGRATIONS
+ VECTOR_MIGRATIONS = VECTOR_MIGRATIONS
+ supports_ttl = True
+ index_config: SqliteIndexConfig | None = None
+ ttl_config: TTLConfig | None = None
+
+ def _get_batch_GET_ops_queries(
+ self, get_ops: Sequence[tuple[int, GetOp]]
+ ) -> list[PreparedGetQuery]:
+ """
+ Build queries to fetch (and optionally refresh the TTL of) multiple keys per namespace.
+
+ Returns a list of PreparedGetQuery objects, which may include:
+ - Queries with kind='refresh' for TTL refresh operations
+ - Queries with kind='get' for data retrieval operations
+ """
+ namespace_groups = defaultdict(list)
+ refresh_ttls = defaultdict(list)
+ for idx, op in get_ops:
+ namespace_groups[op.namespace].append((idx, op.key))
+ refresh_ttls[op.namespace].append(getattr(op, "refresh_ttl", False))
+
+ results = []
+ for namespace, items in namespace_groups.items():
+ _, keys = zip(*items, strict=False)
+ this_refresh_ttls = refresh_ttls[namespace]
+ refresh_ttl_any = any(this_refresh_ttls)
+
+ # Always add the main query to get the data
+ select_query = f"""
+ SELECT key, value, created_at, updated_at, expires_at, ttl_minutes
+ FROM store
+ WHERE prefix = ? AND key IN ({",".join(["?"] * len(keys))})
+ """
+ select_params = (_namespace_to_text(namespace), *keys)
+ results.append(
+ PreparedGetQuery(select_query, select_params, namespace, items, "get")
+ )
+
+ # Add a TTL refresh query if needed
+ if (
+ refresh_ttl_any
+ and self.ttl_config
+ and self.ttl_config.get("refresh_on_read", False)
+ ):
+ placeholders = ",".join(["?"] * len(keys))
+ update_query = f"""
+ UPDATE store
+ SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
+ WHERE prefix = ?
+ AND key IN ({placeholders})
+ AND ttl_minutes IS NOT NULL
+ """
+ update_params = (_namespace_to_text(namespace), *keys)
+ results.append(
+ PreparedGetQuery(
+ update_query, update_params, namespace, items, "refresh"
+ )
+ )
+
+ return results
+
+ def _prepare_batch_PUT_queries(
+ self, put_ops: Sequence[tuple[int, PutOp]]
+ ) -> tuple[
+ list[tuple[str, Sequence]],
+ tuple[str, Sequence[tuple[str, str, str, str]]] | None,
+ ]:
+ # 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 dedupped_ops.values():
+ if op.value is None:
+ deletes.append(op)
+ else:
+ inserts.append(op)
+
+ queries: list[tuple[str, Sequence]] = []
+
+ if deletes:
+ namespace_groups: dict[tuple[str, ...], list[str]] = defaultdict(list)
+ for op in deletes:
+ namespace_groups[op.namespace].append(op.key)
+ for namespace, keys in namespace_groups.items():
+ placeholders = ",".join(["?" for _ in keys])
+ query = (
+ f"DELETE FROM store WHERE prefix = ? AND key IN ({placeholders})"
+ )
+ params = (_namespace_to_text(namespace), *keys)
+ queries.append((query, params))
+
+ embedding_request: tuple[str, Sequence[tuple[str, str, str, str]]] | None = None
+ if inserts:
+ values = []
+ insertion_params = []
+ vector_values = []
+ embedding_request_params = []
+ now = datetime.datetime.now(datetime.timezone.utc)
+
+ # First handle main store insertions
+ for op in inserts:
+ if op.ttl is None:
+ expires_at = None
+ else:
+ expires_at = now + datetime.timedelta(minutes=op.ttl)
+ values.append("(?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ?, ?)")
+ insertion_params.extend(
+ [
+ _namespace_to_text(op.namespace),
+ op.key,
+ orjson.dumps(cast(dict, op.value)),
+ expires_at,
+ op.ttl,
+ ]
+ )
+
+ # 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(
+ "(?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
+ )
+ embedding_request_params.append((ns, k, pathname, text))
+
+ values_str = ",".join(values)
+ query = f"""
+ INSERT OR REPLACE INTO store (prefix, key, value, created_at, updated_at, expires_at, ttl_minutes)
+ VALUES {values_str}
+ """
+ queries.append((query, insertion_params))
+
+ if vector_values:
+ values_str = ",".join(vector_values)
+ query = f"""
+ INSERT OR REPLACE INTO store_vectors (prefix, key, field_name, embedding, created_at, updated_at)
+ VALUES {values_str}
+ """
+ embedding_request = (query, embedding_request_params)
+
+ return queries, embedding_request
+
+ def _prepare_batch_search_queries(
+ self, search_ops: Sequence[tuple[int, SearchOp]]
+ ) -> tuple[
+ list[
+ tuple[str, list[None | str | list[float]], bool]
+ ], # queries, params, needs_refresh
+ list[tuple[int, str]], # idx, query_text pairs to embed
+ ]:
+ """
+ Build per-SearchOp SQL queries (with optional TTL refresh flag) plus embedding requests.
+ Returns:
+ - queries: list of (SQL, param_list, needs_ttl_refresh_flag)
+ - embedding_requests: list of (original_index_in_search_ops, text_query)
+ """
+ queries = []
+ embedding_requests = []
+
+ for idx, (_, op) in enumerate(search_ops):
+ # Build filter conditions first
+ filter_params = []
+ filter_conditions = []
+ if op.filter:
+ for key, value in op.filter.items():
+ _validate_filter_key(key)
+
+ 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:
+ # SQLite json_extract returns unquoted string values
+ if isinstance(value, str):
+ filter_conditions.append(
+ "json_extract(value, '$." + key + "') = ?"
+ )
+ filter_params.append(value)
+ elif value is None:
+ filter_conditions.append(
+ "json_extract(value, '$." + key + "') IS NULL"
+ )
+ elif isinstance(value, bool):
+ # SQLite JSON stores booleans as integers
+ filter_conditions.append(
+ "json_extract(value, '$."
+ + key
+ + "') = "
+ + ("1" if value else "0")
+ )
+ elif isinstance(value, (int, float)):
+ # Use parameterized query to handle special floats and large integers
+ filter_conditions.append(
+ "json_extract(value, '$." + key + "') = ?"
+ )
+ filter_params.append(float(value))
+ else:
+ # Complex objects (list, dict, …) – compare JSON text
+ filter_conditions.append(
+ "json_extract(value, '$." + key + "') = ?"
+ )
+ # orjson.dumps returns bytes → decode to str so SQLite sees TEXT
+ filter_params.append(orjson.dumps(value).decode())
+
+ # Vector search branch
+ if op.query and self.index_config:
+ embedding_requests.append((idx, op.query))
+
+ # Choose the similarity function and score expression based on distance type
+ distance_type = self.index_config.get("distance_type", "cosine")
+
+ if distance_type == "cosine":
+ score_expr = "1.0 - vec_distance_cosine(sv.embedding, ?)"
+ elif distance_type == "l2":
+ score_expr = "vec_distance_L2(sv.embedding, ?)"
+ elif distance_type == "inner_product":
+ # For inner product, we want higher values to be better, so negate the result
+ # since inner product similarity is higher when vectors are more similar
+ score_expr = "-1 * vec_distance_L1(sv.embedding, ?)"
+ else:
+ # Default to cosine similarity
+ score_expr = "1.0 - vec_distance_cosine(sv.embedding, ?)"
+
+ filter_str = (
+ ""
+ if not filter_conditions
+ else " AND " + " AND ".join(filter_conditions)
+ )
+ if op.namespace_prefix:
+ prefix_filter_str = f"WHERE s.prefix LIKE ? {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[5:]} "
+ else:
+ prefix_filter_str = ""
+
+ # We use a CTE to compute scores, with a SQLite-compatible approach for distinct results
+ base_query = f"""
+ WITH scored AS (
+ SELECT s.prefix, s.key, s.value, s.created_at, s.updated_at, s.expires_at, s.ttl_minutes,
+ {score_expr} AS score
+ FROM store s
+ JOIN store_vectors sv ON s.prefix = sv.prefix AND s.key = sv.key
+ {prefix_filter_str}
+ ORDER BY score DESC
+ LIMIT ?
+ ),
+ ranked AS (
+ SELECT prefix, key, value, created_at, updated_at, expires_at, ttl_minutes, score,
+ ROW_NUMBER() OVER (PARTITION BY prefix, key ORDER BY score DESC) as rn
+ FROM scored
+ )
+ SELECT prefix, key, value, created_at, updated_at, expires_at, ttl_minutes, score
+ FROM ranked
+ WHERE rn = 1
+ ORDER BY score DESC
+ LIMIT ?
+ OFFSET ?
+ """
+ params = [
+ _PLACEHOLDER, # Vector placeholder
+ *ns_args,
+ *filter_params,
+ op.limit * 2, # Expanded limit for better results
+ op.limit,
+ op.offset,
+ ]
+ # Regular search branch (no vector search)
+ else:
+ base_query = """
+ SELECT prefix, key, value, created_at, updated_at, expires_at, ttl_minutes, NULL as score
+ FROM store
+ WHERE prefix LIKE ?
+ """
+ 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 ? OFFSET ?"
+ params.extend([op.limit, op.offset])
+
+ # Debug the query
+ logger.debug(f"Search query: {base_query}")
+ logger.debug(f"Search params: {params}")
+
+ # Determine if TTL refresh is needed
+ needs_ttl_refresh = bool(
+ op.refresh_ttl
+ and self.ttl_config
+ and self.ttl_config.get("refresh_on_read", False)
+ )
+
+ # The base_query is now the final_sql, and we pass the refresh flag
+ final_sql = base_query
+ final_params = params
+
+ queries.append((final_sql, final_params, needs_ttl_refresh))
+
+ return queries, embedding_requests
+
+ def _get_batch_list_namespaces_queries(
+ self,
+ list_ops: Sequence[tuple[int, ListNamespacesOp]],
+ ) -> list[tuple[str, Sequence]]:
+ queries: list[tuple[str, Sequence]] = []
+
+ for _, op in list_ops:
+ where_clauses: list[str] = []
+ params: list[Any] = []
+
+ if op.match_conditions:
+ for cond in op.match_conditions:
+ if cond.match_type == "prefix":
+ where_clauses.append("prefix LIKE ?")
+ params.append(
+ f"{_namespace_to_text(cond.path, handle_wildcards=True)}%"
+ )
+ elif cond.match_type == "suffix":
+ where_clauses.append("prefix LIKE ?")
+ params.append(
+ f"%{_namespace_to_text(cond.path, handle_wildcards=True)}"
+ )
+ else:
+ logger.warning(
+ "Unknown match_type in list_namespaces: %s", cond.match_type
+ )
+
+ where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
+
+ if op.max_depth is not None:
+ query = f"""
+ WITH RECURSIVE split(original, truncated, remainder, depth) AS (
+ SELECT
+ prefix AS original,
+ '' AS truncated,
+ prefix AS remainder,
+ 0 AS depth
+ FROM (SELECT DISTINCT prefix FROM store {where_sql})
+
+ UNION ALL
+
+ SELECT
+ original,
+ CASE
+ WHEN depth = 0
+ THEN substr(remainder,
+ 1,
+ CASE
+ WHEN instr(remainder, '.') > 0
+ THEN instr(remainder, '.') - 1
+ ELSE length(remainder)
+ END)
+ ELSE
+ truncated || '.' ||
+ substr(remainder,
+ 1,
+ CASE
+ WHEN instr(remainder, '.') > 0
+ THEN instr(remainder, '.') - 1
+ ELSE length(remainder)
+ END)
+ END AS truncated,
+ CASE
+ WHEN instr(remainder, '.') > 0
+ THEN substr(remainder, instr(remainder, '.') + 1)
+ ELSE ''
+ END AS remainder,
+ depth + 1 AS depth
+ FROM split
+ WHERE remainder <> ''
+ AND depth < ?
+ )
+ SELECT DISTINCT truncated AS prefix
+ FROM split
+ WHERE depth = ? OR remainder = ''
+ ORDER BY prefix
+ LIMIT ? OFFSET ?
+ """
+ params.extend([op.max_depth, op.max_depth, op.limit, op.offset])
+
+ else:
+ query = f"""
+ SELECT DISTINCT prefix
+ FROM store
+ {where_sql}
+ ORDER BY prefix
+ LIMIT ? OFFSET ?
+ """
+ params.extend([op.limit, op.offset])
+
+ 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."""
+ _validate_filter_key(key)
+
+ # We need to properly format values for SQLite JSON extraction comparison
+ if op == "$eq":
+ if isinstance(value, str):
+ return f"json_extract(value, '$.{key}') = ?", [value]
+ elif value is None:
+ return f"json_extract(value, '$.{key}') IS NULL", []
+ elif isinstance(value, bool):
+ # SQLite JSON stores booleans as integers
+ return f"json_extract(value, '$.{key}') = {1 if value else 0}", []
+ elif isinstance(value, (int, float)):
+ # Convert to float to handle inf, -inf, nan, and very large integers
+ # SQLite REAL can handle these cases better than INTEGER
+ return f"json_extract(value, '$.{key}') = ?", [float(value)]
+ else:
+ return f"json_extract(value, '$.{key}') = ?", [orjson.dumps(value)]
+ elif op == "$gt":
+ # For numeric values, SQLite needs to compare as numbers, not strings
+ if isinstance(value, (int, float)):
+ # Convert to float to handle special values and very large integers
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) > ?", [
+ float(value)
+ ]
+ elif isinstance(value, str):
+ return f"json_extract(value, '$.{key}') > ?", [value]
+ else:
+ return f"json_extract(value, '$.{key}') > ?", [orjson.dumps(value)]
+ elif op == "$gte":
+ if isinstance(value, (int, float)):
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) >= ?", [
+ float(value)
+ ]
+ elif isinstance(value, str):
+ return f"json_extract(value, '$.{key}') >= ?", [value]
+ else:
+ return f"json_extract(value, '$.{key}') >= ?", [orjson.dumps(value)]
+ elif op == "$lt":
+ if isinstance(value, (int, float)):
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) < ?", [
+ float(value)
+ ]
+ elif isinstance(value, str):
+ return f"json_extract(value, '$.{key}') < ?", [value]
+ else:
+ return f"json_extract(value, '$.{key}') < ?", [orjson.dumps(value)]
+ elif op == "$lte":
+ if isinstance(value, (int, float)):
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) <= ?", [
+ float(value)
+ ]
+ elif isinstance(value, str):
+ return f"json_extract(value, '$.{key}') <= ?", [value]
+ else:
+ return f"json_extract(value, '$.{key}') <= ?", [orjson.dumps(value)]
+ elif op == "$ne":
+ if isinstance(value, str):
+ return f"json_extract(value, '$.{key}') != ?", [value]
+ elif value is None:
+ return f"json_extract(value, '$.{key}') IS NOT NULL", []
+ elif isinstance(value, bool):
+ return f"json_extract(value, '$.{key}') != {1 if value else 0}", []
+ elif isinstance(value, (int, float)):
+ # Convert to float for consistency
+ return f"json_extract(value, '$.{key}') != ?", [float(value)]
+ else:
+ return f"json_extract(value, '$.{key}') != ?", [orjson.dumps(value)]
+ else:
+ raise ValueError(f"Unsupported operator: {op}")
+
+
+class SqliteStore(BaseSqliteStore, BaseStore):
+ """SQLite-backed store with optional vector search capabilities.
+
+ Examples:
+ Basic setup and usage:
+ ```python
+ from langgraph.store.sqlite import SqliteStore
+ import sqlite3
+
+ conn = sqlite3.connect(":memory:")
+ store = SqliteStore(conn)
+ store.setup() # Run migrations. Done once
+
+ # Store and retrieve data
+ store.put(("users", "123"), "prefs", {"theme": "dark"})
+ item = store.get(("users", "123"), "prefs")
+ ```
+
+ Or using the convenient `from_conn_string` helper:
+
+ ```python
+ from langgraph.store.sqlite import SqliteStore
+
+ with SqliteStore.from_conn_string(":memory:") as store:
+ 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 OpenAIEmbeddings
+ from langgraph.store.sqlite import SqliteStore
+
+ with SqliteStore.from_conn_string(
+ ":memory:",
+ index={
+ "dims": 1536,
+ "embed": OpenAIEmbeddings(),
+ "fields": ["text"] # specify which fields to embed
+ }
+ ) as store:
+ store.setup() # Run migrations
+
+ # Store documents
+ store.put(("docs",), "doc1", {"text": "Python tutorial"})
+ store.put(("docs",), "doc2", {"text": "TypeScript guide"})
+ store.put(("docs",), "doc3", {"text": "Other guide"}, index=False) # don't index
+
+ # Search by similarity
+ results = store.search(("docs",), query="programming guides", limit=2)
+ ```
+
+ 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.
+ """
+
+ MIGRATIONS = MIGRATIONS
+ VECTOR_MIGRATIONS = VECTOR_MIGRATIONS
+ supports_ttl = True
+
+ def __init__(
+ self,
+ conn: sqlite3.Connection,
+ *,
+ deserializer: (
+ Callable[[bytes | str | orjson.Fragment], dict[str, Any]] | None
+ ) = None,
+ index: SqliteIndexConfig | None = None,
+ ttl: TTLConfig | None = None,
+ ):
+ super().__init__()
+ self._deserializer = deserializer
+ self.conn = conn
+ self.lock = threading.Lock()
+ self.is_setup = False
+ self.index_config = index
+ if self.index_config:
+ self.embeddings, self.index_config = _ensure_index_config(self.index_config)
+ else:
+ self.embeddings = None
+ self.ttl_config = ttl
+ self._ttl_sweeper_thread: threading.Thread | None = None
+ self._ttl_stop_event = threading.Event()
+
+ def _get_batch_GET_ops_queries(
+ self, get_ops: Sequence[tuple[int, GetOp]]
+ ) -> list[PreparedGetQuery]:
+ """
+ Build queries to fetch (and optionally refresh the TTL of) multiple keys per namespace.
+
+ Returns a list of PreparedGetQuery objects, which may include:
+ - Queries with kind='refresh' for TTL refresh operations
+ - Queries with kind='get' for data retrieval operations
+ """
+ namespace_groups = defaultdict(list)
+ refresh_ttls = defaultdict(list)
+ for idx, op in get_ops:
+ namespace_groups[op.namespace].append((idx, op.key))
+ refresh_ttls[op.namespace].append(getattr(op, "refresh_ttl", False))
+
+ results = []
+ for namespace, items in namespace_groups.items():
+ _, keys = zip(*items, strict=False)
+ this_refresh_ttls = refresh_ttls[namespace]
+ refresh_ttl_any = any(this_refresh_ttls)
+
+ # Always add the main query to get the data
+ select_query = f"""
+ SELECT key, value, created_at, updated_at, expires_at, ttl_minutes
+ FROM store
+ WHERE prefix = ? AND key IN ({",".join(["?"] * len(keys))})
+ """
+ select_params = (_namespace_to_text(namespace), *keys)
+ results.append(
+ PreparedGetQuery(select_query, select_params, namespace, items, "get")
+ )
+
+ # Add a TTL refresh query if needed
+ if (
+ refresh_ttl_any
+ and self.ttl_config
+ and self.ttl_config.get("refresh_on_read", False)
+ ):
+ placeholders = ",".join(["?"] * len(keys))
+ update_query = f"""
+ UPDATE store
+ SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
+ WHERE prefix = ?
+ AND key IN ({placeholders})
+ AND ttl_minutes IS NOT NULL
+ """
+ update_params = (_namespace_to_text(namespace), *keys)
+ results.append(
+ PreparedGetQuery(
+ update_query, update_params, namespace, items, "refresh"
+ )
+ )
+
+ return results
+
+ def _get_filter_condition(self, key: str, op: str, value: Any) -> tuple[str, list]:
+ """Helper to generate filter conditions."""
+ _validate_filter_key(key)
+
+ # We need to properly format values for SQLite JSON extraction comparison
+ if op == "$eq":
+ if isinstance(value, str):
+ return f"json_extract(value, '$.{key}') = ?", [value]
+ elif value is None:
+ return f"json_extract(value, '$.{key}') IS NULL", []
+ elif isinstance(value, bool):
+ # SQLite JSON stores booleans as integers
+ return f"json_extract(value, '$.{key}') = {1 if value else 0}", []
+ elif isinstance(value, (int, float)):
+ # Convert to float to handle inf, -inf, nan, and very large integers
+ # SQLite REAL can handle these cases better than INTEGER
+ return f"json_extract(value, '$.{key}') = ?", [float(value)]
+ else:
+ return f"json_extract(value, '$.{key}') = ?", [orjson.dumps(value)]
+ elif op == "$gt":
+ # For numeric values, SQLite needs to compare as numbers, not strings
+ if isinstance(value, (int, float)):
+ # Convert to float to handle special values and very large integers
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) > ?", [
+ float(value)
+ ]
+ elif isinstance(value, str):
+ return f"json_extract(value, '$.{key}') > ?", [value]
+ else:
+ return f"json_extract(value, '$.{key}') > ?", [orjson.dumps(value)]
+ elif op == "$gte":
+ if isinstance(value, (int, float)):
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) >= ?", [
+ float(value)
+ ]
+ elif isinstance(value, str):
+ return f"json_extract(value, '$.{key}') >= ?", [value]
+ else:
+ return f"json_extract(value, '$.{key}') >= ?", [orjson.dumps(value)]
+ elif op == "$lt":
+ if isinstance(value, (int, float)):
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) < ?", [
+ float(value)
+ ]
+ elif isinstance(value, str):
+ return f"json_extract(value, '$.{key}') < ?", [value]
+ else:
+ return f"json_extract(value, '$.{key}') < ?", [orjson.dumps(value)]
+ elif op == "$lte":
+ if isinstance(value, (int, float)):
+ return f"CAST(json_extract(value, '$.{key}') AS REAL) <= ?", [
+ float(value)
+ ]
+ elif isinstance(value, str):
+ return f"json_extract(value, '$.{key}') <= ?", [value]
+ else:
+ return f"json_extract(value, '$.{key}') <= ?", [orjson.dumps(value)]
+ elif op == "$ne":
+ if isinstance(value, str):
+ return f"json_extract(value, '$.{key}') != ?", [value]
+ elif value is None:
+ return f"json_extract(value, '$.{key}') IS NOT NULL", []
+ elif isinstance(value, bool):
+ return f"json_extract(value, '$.{key}') != {1 if value else 0}", []
+ elif isinstance(value, (int, float)):
+ # Convert to float for consistency
+ return f"json_extract(value, '$.{key}') != ?", [float(value)]
+ else:
+ return f"json_extract(value, '$.{key}') != ?", [orjson.dumps(value)]
+ else:
+ raise ValueError(f"Unsupported operator: {op}")
+
+ @classmethod
+ @contextmanager
+ def from_conn_string(
+ cls,
+ conn_string: str,
+ *,
+ index: SqliteIndexConfig | None = None,
+ ttl: TTLConfig | None = None,
+ ) -> Iterator[SqliteStore]:
+ """Create a new SqliteStore instance from a connection string.
+
+ Args:
+ conn_string (str): The SQLite connection string.
+ index (Optional[SqliteIndexConfig]): The index configuration for the store.
+ ttl (Optional[TTLConfig]): The time-to-live configuration for the store.
+
+ Returns:
+ SqliteStore: A new SqliteStore instance.
+ """
+ conn = sqlite3.connect(
+ conn_string,
+ check_same_thread=False,
+ isolation_level=None, # autocommit mode
+ )
+ try:
+ yield cls(conn, index=index, ttl=ttl)
+ finally:
+ conn.close()
+
+ @contextmanager
+ def _cursor(self, *, transaction: bool = True) -> Iterator[sqlite3.Cursor]:
+ """Create a database cursor as a context manager.
+
+ Args:
+ transaction (bool): whether to use transaction for the DB operations
+ """
+ if not self.is_setup:
+ self.setup()
+ with self.lock:
+ if transaction:
+ self.conn.execute("BEGIN")
+
+ cur = self.conn.cursor()
+ try:
+ yield cur
+ finally:
+ if transaction:
+ self.conn.execute("COMMIT")
+ cur.close()
+
+ def setup(self) -> None:
+ """Set up the store database.
+
+ This method creates the necessary tables in the SQLite database if they don't
+ already exist and runs database migrations. It should be called before first use.
+ """
+
+ with self.lock:
+ if self.is_setup:
+ return
+ # Create migrations table if it doesn't exist
+ self.conn.executescript(
+ """
+ CREATE TABLE IF NOT EXISTS store_migrations (
+ v INTEGER PRIMARY KEY
+ )
+ """
+ )
+
+ # Check current migration version
+ cur = self.conn.execute(
+ "SELECT v FROM store_migrations ORDER BY v DESC LIMIT 1"
+ )
+ row = cur.fetchone()
+ if row is None:
+ version = -1
+ else:
+ version = row[0]
+
+ # Apply migrations
+ for v, sql in enumerate(self.MIGRATIONS[version + 1 :], start=version + 1):
+ self.conn.executescript(sql)
+ self.conn.execute("INSERT INTO store_migrations (v) VALUES (?)", (v,))
+
+ # Apply vector migrations if index config is provided
+ if self.index_config:
+ # Create vector migrations table if it doesn't exist
+ self.conn.enable_load_extension(True)
+ sqlite_vec.load(self.conn)
+ self.conn.enable_load_extension(False)
+ self.conn.executescript(
+ """
+ CREATE TABLE IF NOT EXISTS vector_migrations (
+ v INTEGER PRIMARY KEY
+ )
+ """
+ )
+
+ # Check current vector migration version
+ cur = self.conn.execute(
+ "SELECT v FROM vector_migrations ORDER BY v DESC LIMIT 1"
+ )
+ row = cur.fetchone()
+ if row is None:
+ version = -1
+ else:
+ version = row[0]
+
+ # Apply vector migrations
+ for v, sql in enumerate(
+ self.VECTOR_MIGRATIONS[version + 1 :], start=version + 1
+ ):
+ self.conn.executescript(sql)
+ self.conn.execute(
+ "INSERT INTO vector_migrations (v) VALUES (?)", (v,)
+ )
+
+ self.is_setup = True
+
+ def sweep_ttl(self) -> int:
+ """Delete expired store items based on TTL.
+
+ Returns:
+ int: The number of deleted items.
+ """
+ with self._cursor() as cur:
+ cur.execute(
+ """
+ DELETE FROM store
+ WHERE expires_at IS NOT NULL AND expires_at < CURRENT_TIMESTAMP
+ """
+ )
+ deleted_count = cur.rowcount
+ return deleted_count
+
+ def start_ttl_sweeper(
+ self, sweep_interval_minutes: int | None = None
+ ) -> concurrent.futures.Future[None]:
+ """Periodically delete expired store items based on TTL.
+
+ Returns:
+ Future that can be waited on or cancelled.
+ """
+ if not self.ttl_config:
+ future: concurrent.futures.Future[None] = concurrent.futures.Future()
+ future.set_result(None)
+ return future
+
+ if self._ttl_sweeper_thread and self._ttl_sweeper_thread.is_alive():
+ logger.info("TTL sweeper thread is already running")
+ # Return a future that can be used to cancel the existing thread
+ future = concurrent.futures.Future()
+ future.add_done_callback(
+ lambda f: self._ttl_stop_event.set() if f.cancelled() else None
+ )
+ return future
+
+ self._ttl_stop_event.clear()
+
+ interval = float(
+ sweep_interval_minutes or self.ttl_config.get("sweep_interval_minutes") or 5
+ )
+ logger.info(f"Starting store TTL sweeper with interval {interval} minutes")
+
+ future = concurrent.futures.Future()
+
+ def _sweep_loop() -> None:
+ try:
+ while not self._ttl_stop_event.is_set():
+ if self._ttl_stop_event.wait(interval * 60):
+ break
+
+ try:
+ expired_items = self.sweep_ttl()
+ if expired_items > 0:
+ logger.info(f"Store swept {expired_items} expired items")
+ except Exception as exc:
+ logger.exception(
+ "Store TTL sweep iteration failed", exc_info=exc
+ )
+ future.set_result(None)
+ except Exception as exc:
+ future.set_exception(exc)
+
+ thread = threading.Thread(target=_sweep_loop, daemon=True, name="ttl-sweeper")
+ self._ttl_sweeper_thread = thread
+ thread.start()
+
+ future.add_done_callback(
+ lambda f: self._ttl_stop_event.set() if f.cancelled() else None
+ )
+ return future
+
+ def stop_ttl_sweeper(self, timeout: float | None = None) -> bool:
+ """Stop the TTL sweeper thread if it's running.
+
+ Args:
+ timeout: Maximum time to wait for the thread to stop, in seconds.
+ If `None`, wait indefinitely.
+
+ Returns:
+ bool: True if the thread was successfully stopped or wasn't running,
+ False if the timeout was reached before the thread stopped.
+ """
+ if not self._ttl_sweeper_thread or not self._ttl_sweeper_thread.is_alive():
+ return True
+
+ logger.info("Stopping TTL sweeper thread")
+ self._ttl_stop_event.set()
+
+ self._ttl_sweeper_thread.join(timeout)
+ success = not self._ttl_sweeper_thread.is_alive()
+
+ if success:
+ self._ttl_sweeper_thread = None
+ logger.info("TTL sweeper thread stopped")
+ else:
+ logger.warning("Timed out waiting for TTL sweeper thread to stop")
+
+ return success
+
+ def __del__(self) -> None:
+ """Ensure the TTL sweeper thread is stopped when the object is garbage collected."""
+ if hasattr(self, "_ttl_stop_event") and hasattr(self, "_ttl_sweeper_thread"):
+ self.stop_ttl_sweeper(timeout=0.1)
+
+ def batch(self, ops: Iterable[Op]) -> list[Result]:
+ """Execute a batch of operations.
+
+ Args:
+ ops (Iterable[Op]): List of operations to execute
+
+ Returns:
+ list[Result]: Results of the operations
+ """
+ grouped_ops, num_ops = _group_ops(ops)
+ results: list[Result] = [None] * num_ops
+
+ with self._cursor(transaction=True) as cur:
+ if GetOp in grouped_ops:
+ self._batch_get_ops(
+ 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:
+ self._batch_list_namespaces_ops(
+ cast(
+ Sequence[tuple[int, ListNamespacesOp]],
+ 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
+
+ def _batch_get_ops(
+ self,
+ get_ops: Sequence[tuple[int, GetOp]],
+ results: list[Result],
+ cur: sqlite3.Cursor,
+ ) -> None:
+ # Group all queries by namespace to execute all operations for each namespace together
+ namespace_queries = defaultdict(list)
+ for prepared_query in self._get_batch_GET_ops_queries(get_ops):
+ namespace_queries[prepared_query.namespace].append(prepared_query)
+
+ # Process each namespace's operations
+ for namespace, queries in namespace_queries.items():
+ # Execute TTL refresh queries first
+ for query in queries:
+ if query.kind == "refresh":
+ try:
+ cur.execute(query.query, query.params)
+ except Exception as e:
+ raise ValueError(
+ f"Error executing TTL refresh: \n{query.query}\n{query.params}\n{e}"
+ ) from e
+
+ # Then execute GET queries and process results
+ for query in queries:
+ if query.kind == "get":
+ try:
+ cur.execute(query.query, query.params)
+ except Exception as e:
+ raise ValueError(
+ f"Error executing GET query: \n{query.query}\n{query.params}\n{e}"
+ ) from e
+
+ rows = cur.fetchall()
+ key_to_row = {
+ row[0]: {
+ "key": row[0],
+ "value": row[1],
+ "created_at": row[2],
+ "updated_at": row[3],
+ "expires_at": row[4] if len(row) > 4 else None,
+ "ttl_minutes": row[5] if len(row) > 5 else None,
+ }
+ for row in rows
+ }
+
+ # Process results for this query
+ for idx, key in query.items:
+ row = key_to_row.get(key)
+ if row:
+ results[idx] = _row_to_item(
+ namespace, row, loader=self._deserializer
+ )
+ else:
+ results[idx] = None
+
+ def _batch_put_ops(
+ self,
+ put_ops: Sequence[tuple[int, PutOp]],
+ cur: sqlite3.Cursor,
+ ) -> None:
+ 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]
+ )
+
+ # Convert vectors to SQLite-friendly format
+ vector_params = []
+ for (ns, k, pathname, _), vector in zip(txt_params, vectors, strict=False):
+ vector_params.extend(
+ [ns, k, pathname, sqlite_vec.serialize_float32(vector)]
+ )
+
+ queries.append((query, vector_params))
+
+ for query, params in queries:
+ cur.execute(query, params)
+
+ def _batch_search_ops(
+ self,
+ search_ops: Sequence[tuple[int, SearchOp]],
+ results: list[Result],
+ cur: sqlite3.Cursor,
+ ) -> None:
+ prepared_queries, embedding_requests = self._prepare_batch_search_queries(
+ search_ops
+ )
+
+ # Setup similarity functions if they don't exist
+ if embedding_requests and self.embeddings:
+ # Generate embeddings for search queries
+ embeddings = self.embeddings.embed_documents(
+ [query for _, query in embedding_requests]
+ )
+
+ # Replace placeholders with actual embeddings
+ for (embed_req_idx, _), embedding in zip(
+ embedding_requests, embeddings, strict=False
+ ):
+ if embed_req_idx < len(prepared_queries):
+ _params_list: list = prepared_queries[embed_req_idx][1]
+ for i, param in enumerate(_params_list):
+ if param is _PLACEHOLDER:
+ _params_list[i] = sqlite_vec.serialize_float32(embedding)
+ else:
+ logger.warning(
+ f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
+ )
+
+ for (original_op_idx, _), (query, params, needs_refresh) in zip(
+ search_ops, prepared_queries, strict=False
+ ):
+ cur.execute(query, params)
+ rows = cur.fetchall()
+
+ if needs_refresh and rows and self.ttl_config:
+ keys_to_refresh = []
+ for row_data in rows:
+ keys_to_refresh.append((row_data[0], row_data[1]))
+
+ if keys_to_refresh:
+ updates_by_prefix = defaultdict(list)
+ for prefix_text, key_text in keys_to_refresh:
+ updates_by_prefix[prefix_text].append(key_text)
+
+ for prefix_text, key_list in updates_by_prefix.items():
+ placeholders = ",".join(["?"] * len(key_list))
+ update_query = f"""
+ UPDATE store
+ SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
+ WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
+ """
+ update_params = (prefix_text, *key_list)
+ try:
+ cur.execute(update_query, update_params)
+ except Exception as e:
+ logger.error(
+ f"Error during TTL refresh update for search: {e}"
+ )
+
+ if "score" in query: # Vector search query
+ items = [
+ _row_to_search_item(
+ _decode_ns_text(row[0]),
+ {
+ "key": row[1],
+ "value": row[2],
+ "created_at": row[3],
+ "updated_at": row[4],
+ "expires_at": row[5] if len(row) > 5 else None,
+ "ttl_minutes": row[6] if len(row) > 6 else None,
+ "score": row[7] if len(row) > 7 else None,
+ },
+ loader=self._deserializer,
+ )
+ for row in rows
+ ]
+ else: # Regular search query
+ items = [
+ _row_to_search_item(
+ _decode_ns_text(row[0]),
+ {
+ "key": row[1],
+ "value": row[2],
+ "created_at": row[3],
+ "updated_at": row[4],
+ "expires_at": row[5] if len(row) > 5 else None,
+ "ttl_minutes": row[6] if len(row) > 6 else None,
+ },
+ loader=self._deserializer,
+ )
+ for row in rows
+ ]
+
+ results[original_op_idx] = items
+
+ def _batch_list_namespaces_ops(
+ self,
+ list_ops: Sequence[tuple[int, ListNamespacesOp]],
+ results: list[Result],
+ cur: sqlite3.Cursor,
+ ) -> None:
+ queries = self._get_batch_list_namespaces_queries(list_ops)
+ for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
+ cur.execute(query, params)
+ results[idx] = [_decode_ns_text(row[0]) for row in cur.fetchall()]
+
+ async def abatch(self, ops: Iterable[Op]) -> list[Result]:
+ """Async batch operation - not supported in SqliteStore.
+
+ Use AsyncSqliteStore for async operations.
+ """
+ raise NotImplementedError(_AIO_ERROR_MSG)
+
+
+# Helper functions
+
+
+def _ensure_index_config(
+ index_config: SqliteIndexConfig,
+) -> tuple[Any, SqliteIndexConfig]:
+ """Process and validate index configuration."""
+ index_config = index_config.copy()
+ tokenized: list[tuple[str, 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/langgraph/source/libs/checkpoint-sqlite/pyproject.toml b/langgraph/source/libs/checkpoint-sqlite/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..aa587779602621936dacb42e84ef57ceb9830059
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/pyproject.toml
@@ -0,0 +1,84 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "langgraph-checkpoint-sqlite"
+version = "3.0.3"
+description = "Library with a SQLite implementation of LangGraph checkpoint saver."
+authors = []
+requires-python = ">=3.10"
+readme = "README.md"
+license = "MIT"
+license-files = ['LICENSE']
+dependencies = [
+ "langgraph-checkpoint>=3,<5.0.0",
+ "aiosqlite>=0.20",
+ "sqlite-vec>=0.1.6",
+]
+
+[project.urls]
+Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-sqlite"
+Twitter = "https://x.com/LangChain"
+Slack = "https://www.langchain.com/join-community"
+Reddit = "https://www.reddit.com/r/LangChain/"
+
+[dependency-groups]
+test = [
+ "pytest",
+ "pytest-asyncio",
+ "pytest-mock",
+ "pytest-watcher",
+ "langgraph-checkpoint",
+ "pytest-retry>=1.7.0",
+]
+lint = [
+ "ruff",
+ "codespell",
+ "mypy",
+]
+dev = [
+ {include-group = "test"},
+ {include-group = "lint"},
+]
+
+[tool.uv]
+default-groups = ['dev']
+
+[tool.uv.sources]
+langgraph-checkpoint = { path = "../checkpoint", editable = true }
+
+[tool.hatch.build.targets.wheel]
+include = ["langgraph"]
+
+[tool.pytest.ini_options]
+addopts = "--strict-markers --strict-config --durations=5 -vv"
+asyncio_mode = "auto"
+
+[tool.ruff]
+lint.select = [
+ "E", # pycodestyle
+ "F", # Pyflakes
+ "UP", # pyupgrade
+ "B", # flake8-bugbear
+ "I", # isort
+ "UP", # pyupgrade
+]
+lint.ignore = ["E501", "B008"]
+target-version = "py310"
+
+[tool.pytest-watcher]
+now = true
+delay = 0.1
+runner_args = ["--ff", "-v", "--tb", "short"]
+patterns = ["*.py"]
+
+[tool.mypy]
+# https://mypy.readthedocs.io/en/stable/config_file.html
+disallow_untyped_defs = "True"
+explicit_package_bases = "True"
+warn_no_return = "False"
+warn_unused_ignores = "True"
+warn_redundant_casts = "True"
+allow_redefinition = "True"
+disable_error_code = "typeddict-item, return-value"
diff --git a/langgraph/source/libs/checkpoint-sqlite/tests/__init__.py b/langgraph/source/libs/checkpoint-sqlite/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/checkpoint-sqlite/tests/test_aiosqlite.py b/langgraph/source/libs/checkpoint-sqlite/tests/test_aiosqlite.py
new file mode 100644
index 0000000000000000000000000000000000000000..36d3294075c96837a6706a8b1225a513868e50fb
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/tests/test_aiosqlite.py
@@ -0,0 +1,190 @@
+from typing import Any
+
+import pytest
+from langchain_core.runnables import RunnableConfig
+from langgraph.checkpoint.base import (
+ Checkpoint,
+ CheckpointMetadata,
+ create_checkpoint,
+ empty_checkpoint,
+)
+
+from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
+
+
+class TestAsyncSqliteSaver:
+ @pytest.fixture(autouse=True)
+ def setup(self) -> None:
+ # objects for test setup
+ self.config_1: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-1",
+ "checkpoint_id": "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",
+ }
+ }
+
+ 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,
+ }
+ self.metadata_2: CheckpointMetadata = {
+ "source": "loop",
+ "step": 1,
+ "writes": {"foo": "bar"},
+ "score": None,
+ }
+ self.metadata_3: CheckpointMetadata = {}
+
+ async def test_combined_metadata(self) -> None:
+ async with AsyncSqliteSaver.from_conn_string(":memory:") as saver:
+ config: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-2",
+ "checkpoint_ns": "",
+ "__super_private_key": "super_private_value",
+ },
+ "metadata": {"run_id": "my_run_id"},
+ }
+ await saver.aput(config, self.chkpnt_2, self.metadata_2, {})
+ checkpoint = await saver.aget_tuple(config)
+ assert checkpoint is not None and checkpoint.metadata == {
+ **self.metadata_2,
+ "run_id": "my_run_id",
+ }
+
+ async def test_asearch(self) -> None:
+ async with AsyncSqliteSaver.from_conn_string(":memory:") 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, {})
+
+ # 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_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_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_3 = [c async for c in saver.alist(None, filter=query_3)]
+ assert len(search_results_3) == 3
+
+ 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"}
+
+ # Test limit param
+ search_results_6 = [
+ c
+ async for c in saver.alist(
+ {"configurable": {"thread_id": "thread-2"}}, limit=1
+ )
+ ]
+ assert len(search_results_6) == 1
+ assert search_results_6[0].config["configurable"]["thread_id"] == "thread-2"
+
+ # Test before param
+ search_results_7 = [
+ c async for c in saver.alist(None, before=search_results_5[1].config)
+ ]
+ assert len(search_results_7) == 1
+ assert search_results_7[0].config["configurable"]["thread_id"] == "thread-1"
+
+ async def test_limit_parameter_sql_injection_prevention(self) -> None:
+ """Test that the limit parameter properly uses parameterized queries to prevent SQL injection."""
+ async with AsyncSqliteSaver.from_conn_string(":memory:") as saver:
+ # Setup: Create multiple checkpoints
+ for i in range(5):
+ config: RunnableConfig = {
+ "configurable": {
+ "thread_id": f"thread-{i}",
+ "checkpoint_ns": "",
+ }
+ }
+ checkpoint = empty_checkpoint()
+ metadata: CheckpointMetadata = {"index": i}
+ await saver.aput(config, checkpoint, metadata, {})
+
+ # Test that limit works correctly with valid integer
+ results = [c async for c in saver.alist(None, limit=2)]
+ assert len(results) == 2
+
+ # Test that limit=0 returns no results
+ results = [c async for c in saver.alist(None, limit=0)]
+ assert len(results) == 0
+
+ # Test that limit=None returns all results
+ results = [c async for c in saver.alist(None, limit=None)]
+ assert len(results) == 5
+
+ # Test explicit SQL injection attempt via limit parameter
+ # Even if type checking is bypassed and a malicious string is passed,
+ # the parameterized query will treat it as a value, not SQL code
+ # This would cause an error (can't convert string to int for LIMIT),
+ # which is the correct secure behavior
+ malicious_limits = [
+ "1; DROP TABLE checkpoints; --",
+ "1 OR 1=1",
+ "999999 UNION SELECT * FROM checkpoints",
+ ]
+
+ for malicious_limit in malicious_limits:
+ # The parameterized query should safely reject non-integer limits
+ # or convert them in a way that prevents SQL injection
+ try:
+ # Bypass type checking by casting
+ results = [
+ c
+ async for c in saver.alist(None, limit=malicious_limit) # type: ignore
+ ]
+ # If it doesn't raise an error, it should at least not execute the injection
+ # SQLite's parameter binding will try to convert the string to an integer
+ # which will either fail or treat it as 0
+ except Exception:
+ # Expected: SQLite should reject invalid limit values
+ pass
+
+ # Verify the checkpoints table still exists and has all data
+ # (would have been dropped if injection succeeded)
+ results = [c async for c in saver.alist(None, limit=None)]
+ assert len(results) == 5
diff --git a/langgraph/source/libs/checkpoint-sqlite/tests/test_async_store.py b/langgraph/source/libs/checkpoint-sqlite/tests/test_async_store.py
new file mode 100644
index 0000000000000000000000000000000000000000..d771c22390a5b5fc30cc3c77e20a22c92146f5dd
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/tests/test_async_store.py
@@ -0,0 +1,719 @@
+# mypy: disable-error-code="union-attr,arg-type,index,operator"
+import asyncio
+import os
+import tempfile
+import uuid
+from collections.abc import AsyncIterator, Generator, Iterable
+from contextlib import asynccontextmanager
+from typing import cast
+
+import pytest
+from langgraph.store.base import (
+ GetOp,
+ Item,
+ ListNamespacesOp,
+ PutOp,
+ SearchOp,
+)
+
+from langgraph.store.sqlite import AsyncSqliteStore
+from langgraph.store.sqlite.base import SqliteIndexConfig
+from tests.test_store import CharacterEmbeddings
+
+
+@pytest.fixture(scope="function", params=["memory", "file"])
+async def store(request: pytest.FixtureRequest) -> AsyncIterator[AsyncSqliteStore]:
+ """Create an AsyncSqliteStore for testing."""
+ if request.param == "memory":
+ # In-memory store
+ async with AsyncSqliteStore.from_conn_string(":memory:") as store:
+ await store.setup()
+ yield store
+ else:
+ # Temporary file store
+ temp_file = tempfile.NamedTemporaryFile(delete=False)
+ temp_file.close()
+ try:
+ async with AsyncSqliteStore.from_conn_string(temp_file.name) as store:
+ await store.setup()
+ yield store
+ finally:
+ os.unlink(temp_file.name)
+
+
+@pytest.fixture(scope="function")
+def fake_embeddings() -> CharacterEmbeddings:
+ """Create fake embeddings for testing."""
+ return CharacterEmbeddings(dims=500)
+
+
+@asynccontextmanager
+async def create_vector_store(
+ fake_embeddings: CharacterEmbeddings,
+ conn_string: str = ":memory:",
+ text_fields: list[str] | None = None,
+) -> AsyncIterator[AsyncSqliteStore]:
+ """Create an AsyncSqliteStore with vector search capabilities."""
+ index_config: SqliteIndexConfig = {
+ "dims": fake_embeddings.dims,
+ "embed": fake_embeddings,
+ "text_fields": text_fields,
+ }
+
+ async with AsyncSqliteStore.from_conn_string(
+ conn_string, index=index_config
+ ) as store:
+ await store.setup()
+ yield store
+
+
+@pytest.fixture(scope="function", params=["memory", "file"])
+def conn_string(request: pytest.FixtureRequest) -> Generator[str, None, None]:
+ if request.param == "memory":
+ yield ":memory:"
+ else:
+ temp_file = tempfile.NamedTemporaryFile(delete=False)
+ temp_file.close()
+ try:
+ yield temp_file.name
+ finally:
+ os.unlink(temp_file.name)
+
+
+async def test_no_running_loop(store: AsyncSqliteStore) -> None:
+ """Test that sync methods raise proper errors in the main thread."""
+ with pytest.raises(asyncio.InvalidStateError):
+ store.put(("foo", "bar"), "baz", {"val": "baz"})
+ with pytest.raises(asyncio.InvalidStateError):
+ store.get(("foo", "bar"), "baz")
+ with pytest.raises(asyncio.InvalidStateError):
+ store.delete(("foo", "bar"), "baz")
+ with pytest.raises(asyncio.InvalidStateError):
+ store.search(("foo", "bar"))
+ with pytest.raises(asyncio.InvalidStateError):
+ store.list_namespaces(prefix=("foo",))
+ with pytest.raises(asyncio.InvalidStateError):
+ store.batch([PutOp(namespace=("foo", "bar"), key="baz", value={"val": "baz"})])
+
+
+async def test_large_batches_async(store: AsyncSqliteStore) -> None:
+ """Test processing large batch operations asynchronously."""
+ N = 100
+ M = 10
+ coros = []
+ for m in range(M):
+ for i in range(N):
+ coros.append(
+ store.aput(
+ ("test", "foo", "bar", "baz", str(m % 2)),
+ f"key{i}",
+ value={"foo": "bar" + str(i)},
+ )
+ )
+ coros.append(
+ asyncio.create_task(
+ store.aget(
+ ("test", "foo", "bar", "baz", str(m % 2)),
+ f"key{i}",
+ )
+ )
+ )
+ coros.append(
+ asyncio.create_task(
+ store.alist_namespaces(
+ prefix=None,
+ max_depth=m + 1,
+ )
+ )
+ )
+ coros.append(
+ asyncio.create_task(
+ store.asearch(
+ ("test",),
+ )
+ )
+ )
+ coros.append(
+ store.aput(
+ ("test", "foo", "bar", "baz", str(m % 2)),
+ f"key{i}",
+ value={"foo": "bar" + str(i)},
+ )
+ )
+ coros.append(
+ store.adelete(
+ ("test", "foo", "bar", "baz", str(m % 2)),
+ f"key{i}",
+ )
+ )
+
+ results = await asyncio.gather(*coros)
+ assert len(results) == M * N * 6
+
+
+async def test_abatch_order(store: AsyncSqliteStore) -> None:
+ """Test ordering of batch operations in async context."""
+ # Setup test data
+ await store.aput(("test", "foo"), "key1", {"data": "value1"})
+ await store.aput(("test", "bar"), "key2", {"data": "value2"})
+
+ ops = [
+ 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(
+ cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
+ )
+ 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 # Put operation returns None
+ assert isinstance(results[2], list)
+ # SQLite query implementation might return different results
+ # Just check that we get a list back and don't check the exact content
+ assert isinstance(results[3], list)
+ assert len(results[3]) > 0
+ 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", "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", "foo"), key="key1"),
+ ]
+
+ results_reordered = await store.abatch(
+ cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops_reordered)
+ )
+ assert len(results_reordered) == 5
+ assert isinstance(results_reordered[0], list)
+ 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 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"
+
+
+async def test_batch_get_ops(store: AsyncSqliteStore) -> None:
+ """Test GET operations in batch context."""
+ # Setup test data
+ await store.aput(("test",), "key1", {"data": "value1"})
+ await store.aput(("test",), "key2", {"data": "value2"})
+
+ ops = [
+ GetOp(namespace=("test",), key="key1"),
+ GetOp(namespace=("test",), key="key2"),
+ GetOp(namespace=("test",), key="key3"), # Non-existent key
+ ]
+
+ results = await store.abatch(ops)
+
+ assert len(results) == 3
+ assert results[0] is not None
+ assert results[1] is not None
+ assert results[2] is None
+ if results[0] is not None:
+ assert results[0].key == "key1"
+ if results[1] is not None:
+ assert results[1].key == "key2"
+
+
+async def test_batch_put_ops(store: AsyncSqliteStore) -> None:
+ """Test PUT operations in batch context."""
+ ops = [
+ PutOp(namespace=("test",), key="key1", value={"data": "value1"}),
+ PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
+ PutOp(namespace=("test",), key="key3", value=None), # Delete operation
+ ]
+
+ results = await store.abatch(ops)
+ assert len(results) == 3
+ assert all(result is None for result in results)
+
+ # 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: AsyncSqliteStore) -> None:
+ """Test SEARCH operations in batch context."""
+ # Setup test data
+ await store.aput(("test", "foo"), "key1", {"data": "value1"})
+ await store.aput(("test", "bar"), "key2", {"data": "value2"})
+
+ ops = [
+ SearchOp(
+ namespace_prefix=("test",), filter={"data": "value1"}, limit=10, offset=0
+ ),
+ SearchOp(namespace_prefix=("test",), filter=None, limit=5, offset=0),
+ ]
+
+ results = await store.abatch(ops)
+
+ assert len(results) == 2
+ # SQLite query implementation might return different results
+ # Just check that we get lists back and don't check the exact content
+ assert isinstance(results[0], list)
+ assert isinstance(results[1], list)
+ assert len(results[1]) >= 1 # We should at least find some results
+
+
+async def test_batch_list_namespaces_ops(store: AsyncSqliteStore) -> None:
+ """Test LIST NAMESPACES operations in batch context."""
+ # 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
+ if isinstance(results[0], list):
+ assert len(results[0]) == 2
+ assert ("test", "namespace1") in results[0]
+ assert ("test", "namespace2") in results[0]
+
+
+async def test_vector_store_initialization(
+ fake_embeddings: CharacterEmbeddings,
+) -> None:
+ """Test store initialization with embedding config."""
+ async with create_vector_store(fake_embeddings) as store:
+ assert store.index_config is not None
+ assert store.index_config["dims"] == fake_embeddings.dims
+ if hasattr(store.index_config.get("embed"), "embed_documents"):
+ assert store.index_config["embed"] == fake_embeddings
+
+
+async def test_vector_insert_with_auto_embedding(
+ fake_embeddings: CharacterEmbeddings,
+ conn_string: str,
+) -> None:
+ """Test inserting items that get auto-embedded."""
+ async with create_vector_store(fake_embeddings, conn_string=conn_string) as store:
+ 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
+
+
+async def test_vector_update_with_embedding(
+ fake_embeddings: CharacterEmbeddings,
+ conn_string: str,
+) -> None:
+ """Test that updating items properly updates their embeddings."""
+ async with create_vector_store(fake_embeddings, conn_string=conn_string) as store:
+ 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].score is not None
+ 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
+ and initial_score is not None
+ and 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
+ and after_score is not None
+ and 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)
+
+
+async def test_vector_search_with_filters(
+ fake_embeddings: CharacterEmbeddings,
+ conn_string: str,
+) -> None:
+ """Test combining vector search with filters."""
+ async with create_vector_store(fake_embeddings, conn_string=conn_string) as store:
+ 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)
+
+ # Vector search with filters can be inconsistent in test environments
+ # Skip asserting exact results as we've already validated the functionality
+ # in the synchronous tests
+ _ = await store.asearch(("test",), query="apple", filter={"color": "red"})
+
+ # Skip asserting exact results as we've already validated the functionality
+ # in the synchronous tests
+ _ = await store.asearch(("test",), query="car", filter={"color": "red"})
+
+ # Skip asserting exact results as we've already validated the functionality
+ # in the synchronous tests
+ _ = await store.asearch(
+ ("test",), query="bbbbluuu", filter={"score": {"$gt": 3.2}}
+ )
+
+ # Skip asserting exact results as we've already validated the functionality
+ # in the synchronous tests
+ _ = await store.asearch(
+ ("test",), query="apple", filter={"score": {"$gte": 4.0}, "color": "green"}
+ )
+
+
+async def test_vector_search_pagination(fake_embeddings: CharacterEmbeddings) -> None:
+ """Test pagination with vector search."""
+ async with create_vector_store(fake_embeddings) as store:
+ 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_vector_search_edge_cases(fake_embeddings: CharacterEmbeddings) -> None:
+ """Test edge cases in vector search."""
+ async with create_vector_store(fake_embeddings) as store:
+ await store.aput(("test",), "doc1", {"text": "test document"})
+
+ results = await store.asearch(("test",), query="")
+ assert len(results) == 1
+
+ results = await store.asearch(("test",), query=None)
+ assert len(results) == 1
+
+ long_query = "test " * 100
+ results = await store.asearch(("test",), query=long_query)
+ assert len(results) == 1
+
+ special_query = "test!@#$%^&*()"
+ results = await store.asearch(("test",), query=special_query)
+ assert len(results) == 1
+
+
+async def test_embed_with_path(
+ fake_embeddings: CharacterEmbeddings,
+) -> None:
+ """Test vector search with specific text fields in SQLite store."""
+ async with create_vector_store(
+ 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)
+
+ # 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
+ assert results[0].score > 0.9
+ assert results[1].score > 0.9
+
+ # ~Only match doc2
+ 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
+
+ # 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 < 0.9
+ assert results[1].score < 0.9
+
+
+async def test_basic_store_ops(
+ fake_embeddings: CharacterEmbeddings,
+) -> None:
+ """Test vector search with specific text fields in SQLite store."""
+ async with create_vector_store(
+ fake_embeddings, text_fields=["key0", "key1", "key3"]
+ ) as store:
+ uid = uuid.uuid4().hex
+ namespace = (uid, "test", "documents")
+ item_id = "doc1"
+ item_value = {"title": "Test Document", "content": "Hello, World!"}
+ results = await store.asearch((uid,))
+ assert len(results) == 0
+
+ await store.aput(namespace, item_id, item_value)
+ item = await store.aget(namespace, item_id)
+
+ assert item is not None
+ assert item.namespace == namespace
+ assert item.key == item_id
+ assert item.value == item_value
+ assert item.created_at is not None
+ assert item.updated_at is not None
+
+ updated_value = {
+ "title": "Updated Test Document",
+ "content": "Hello, LangGraph!",
+ }
+ await asyncio.sleep(1.01)
+ await store.aput(namespace, item_id, updated_value)
+ updated_item = await store.aget(namespace, item_id)
+ assert updated_item is not None
+
+ assert updated_item.value == updated_value
+ assert updated_item.updated_at > item.updated_at
+ different_namespace = (uid, "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)
+
+ items = await store.asearch((uid, "test"), limit=10)
+ 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 = await store.alist_namespaces(prefix=(uid, "test"))
+ assert (uid, "test", "documents") in namespaces
+
+ 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
+
+ deleted_item = await store.aget(namespace, new_item_id)
+ assert deleted_item is None
+
+ empty_search_results = await store.asearch((uid, "test"), limit=10)
+ assert len(empty_search_results) == 0
+
+
+async def test_list_namespaces(
+ fake_embeddings: CharacterEmbeddings,
+) -> None:
+ """Test list namespaces functionality with various filters."""
+ async with create_vector_store(
+ fake_embeddings, text_fields=["key0", "key1", "key3"]
+ ) 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),
+ ]
+
+ # Add test data
+ for namespace in test_namespaces:
+ await store.aput(namespace, "dummy", {"content": "dummy"})
+
+ # Test prefix filtering
+ 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)
+
+ # Test specific prefix
+ 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)
+
+ # Test suffix filtering
+ 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)
+
+ # Test combined prefix and suffix
+ 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
+ )
+
+ # Test wildcard in prefix
+ 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)
+
+ # Test wildcard in suffix
+ 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,
+ )
+
+ # Test max depth
+ 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(res for res in max_depth_result)) == len(max_depth_result) == 5
+
+ # Test pagination
+ limit_result = await store.alist_namespaces(prefix=(test_pref,), limit=3)
+ assert len(limit_result) == 3
+
+ offset_result = await store.alist_namespaces(prefix=(test_pref,), offset=3)
+ assert len(offset_result) == len(test_namespaces) - 3
+
+ empty_prefix_result = await store.alist_namespaces(prefix=(test_pref,))
+ assert len(empty_prefix_result) == len(test_namespaces)
+ assert set(empty_prefix_result) == set(test_namespaces)
+
+ # Clean up
+ for namespace in test_namespaces:
+ await store.adelete(namespace, "dummy")
+
+
+async def test_search_items(
+ fake_embeddings: CharacterEmbeddings,
+) -> None:
+ """Test search_items functionality by calling store methods directly."""
+ base = "test_search_items"
+ test_namespaces = [
+ (base, "documents", "user1"),
+ (base, "documents", "user2"),
+ (base, "reports", "department1"),
+ (base, "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"]},
+ ]
+
+ async with create_vector_store(
+ fake_embeddings, text_fields=["key0", "key1", "key3"]
+ ) as store:
+ # Insert test data
+ for ns, item in zip(test_namespaces, test_items, strict=False):
+ key = f"item_{ns[-1]}"
+ await store.aput(ns, key, item)
+
+ # 1. Search documents
+ docs = await store.asearch((base, "documents"))
+ assert len(docs) == 2
+ assert all(item.namespace[1] == "documents" for item in docs)
+
+ # 2. Search reports
+ reports = await store.asearch((base, "reports"))
+ assert len(reports) == 2
+ assert all(item.namespace[1] == "reports" for item in reports)
+
+ # 3. Pagination
+ first_page = await store.asearch((base,), limit=2, offset=0)
+ second_page = await store.asearch((base,), limit=2, offset=2)
+ assert len(first_page) == 2
+ assert len(second_page) == 2
+ keys_page1 = {item.key for item in first_page}
+ keys_page2 = {item.key for item in second_page}
+ assert keys_page1.isdisjoint(keys_page2)
+ all_items = await store.asearch((base,))
+ assert len(all_items) == 4
+
+ john_items = await store.asearch((base,), filter={"author": "John Doe"})
+ assert len(john_items) == 2
+ assert all(item.value["author"] == "John Doe" for item in john_items)
+
+ draft_items = await store.asearch((base,), filter={"tags": ["draft"]})
+ assert len(draft_items) == 2
+ assert all("draft" in item.value["tags"] for item in draft_items)
+
+ for ns in test_namespaces:
+ key = f"item_{ns[-1]}"
+ await store.adelete(ns, key)
diff --git a/langgraph/source/libs/checkpoint-sqlite/tests/test_sqlite.py b/langgraph/source/libs/checkpoint-sqlite/tests/test_sqlite.py
new file mode 100644
index 0000000000000000000000000000000000000000..9eda0bc2cd261970e12806a494119acba3a95901
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/tests/test_sqlite.py
@@ -0,0 +1,309 @@
+from typing import Any, cast
+
+import pytest
+from langchain_core.runnables import RunnableConfig
+from langgraph.checkpoint.base import (
+ Checkpoint,
+ CheckpointMetadata,
+ create_checkpoint,
+ empty_checkpoint,
+)
+
+from langgraph.checkpoint.sqlite import SqliteSaver
+from langgraph.checkpoint.sqlite.utils import _metadata_predicate, search_where
+
+
+class TestSqliteSaver:
+ @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
+ "checkpoint_id": "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",
+ }
+ }
+
+ 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,
+ }
+ self.metadata_2: CheckpointMetadata = {
+ "source": "loop",
+ "step": 1,
+ "writes": {"foo": "bar"},
+ "score": None,
+ }
+ self.metadata_3: CheckpointMetadata = {}
+
+ def test_combined_metadata(self) -> None:
+ with SqliteSaver.from_conn_string(":memory:") as saver:
+ config: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-2",
+ "checkpoint_ns": "",
+ "__super_private_key": "super_private_value",
+ },
+ "metadata": {"run_id": "my_run_id"},
+ }
+ saver.put(config, self.chkpnt_2, self.metadata_2, {})
+ checkpoint = saver.get_tuple(config)
+ assert checkpoint is not None and checkpoint.metadata == {
+ **self.metadata_2,
+ "run_id": "my_run_id",
+ }
+
+ def test_search(self) -> None:
+ with SqliteSaver.from_conn_string(":memory:") as saver:
+ # set up test
+ # 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, {})
+
+ # 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_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_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_3 = list(saver.list(None, filter=query_3))
+ assert len(search_results_3) == 3
+
+ 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"}
+
+ # search with before param
+ search_results_6 = list(saver.list(None, before=search_results_5[1].config))
+ assert len(search_results_6) == 1
+ assert search_results_6[0].config["configurable"]["thread_id"] == "thread-1"
+
+ # search with limit param
+ search_results_7 = list(
+ saver.list({"configurable": {"thread_id": "thread-2"}}, limit=1)
+ )
+ assert len(search_results_7) == 1
+ assert search_results_7[0].config["configurable"]["thread_id"] == "thread-2"
+
+ def test_search_where(self) -> None:
+ # call method / assertions
+ expected_predicate_1 = "WHERE json_extract(CAST(metadata AS TEXT), '$.source') = ? AND json_extract(CAST(metadata AS TEXT), '$.step') = ? AND json_extract(CAST(metadata AS TEXT), '$.writes') = ? AND json_extract(CAST(metadata AS TEXT), '$.score') = ? AND checkpoint_id < ?"
+ expected_param_values_1 = ["input", 2, "{}", 1, "1"]
+ assert search_where(
+ None, cast(dict[str, Any], self.metadata_1), self.config_1
+ ) == (
+ expected_predicate_1,
+ expected_param_values_1,
+ )
+
+ def test_metadata_predicate(self) -> None:
+ # call method / assertions
+ expected_predicate_1 = [
+ "json_extract(CAST(metadata AS TEXT), '$.source') = ?",
+ "json_extract(CAST(metadata AS TEXT), '$.step') = ?",
+ "json_extract(CAST(metadata AS TEXT), '$.writes') = ?",
+ "json_extract(CAST(metadata AS TEXT), '$.score') = ?",
+ ]
+ expected_predicate_2 = [
+ "json_extract(CAST(metadata AS TEXT), '$.source') = ?",
+ "json_extract(CAST(metadata AS TEXT), '$.step') = ?",
+ "json_extract(CAST(metadata AS TEXT), '$.writes') = ?",
+ "json_extract(CAST(metadata AS TEXT), '$.score') IS ?",
+ ]
+ expected_predicate_3: list[str] = []
+
+ expected_param_values_1 = ["input", 2, "{}", 1]
+ expected_param_values_2 = ["loop", 1, '{"foo":"bar"}', None]
+ expected_param_values_3: list[Any] = []
+
+ assert _metadata_predicate(cast(dict[str, Any], self.metadata_1)) == (
+ expected_predicate_1,
+ expected_param_values_1,
+ )
+ assert _metadata_predicate(cast(dict[str, Any], self.metadata_2)) == (
+ expected_predicate_2,
+ expected_param_values_2,
+ )
+ assert _metadata_predicate(cast(dict[str, Any], self.metadata_3)) == (
+ expected_predicate_3,
+ expected_param_values_3,
+ )
+
+ async def test_informative_async_errors(self) -> None:
+ with SqliteSaver.from_conn_string(":memory:") as saver:
+ # call method / assertions
+ with pytest.raises(NotImplementedError, match="AsyncSqliteSaver"):
+ await saver.aget(self.config_1)
+ with pytest.raises(NotImplementedError, match="AsyncSqliteSaver"):
+ await saver.aget_tuple(self.config_1)
+ with pytest.raises(NotImplementedError, match="AsyncSqliteSaver"):
+ async for _ in saver.alist(self.config_1):
+ pass
+
+ def test_metadata_predicate_sql_injection_prevention(self) -> None:
+ """Test that _metadata_predicate rejects malicious filter keys."""
+ # Test various SQL injection payloads
+ malicious_keys = [
+ "x') OR '1'='1", # Boolean-based injection
+ "x') OR 1=1 --", # Comment-based injection
+ "x') UNION SELECT 1,2,3,4,5,6,7 --", # UNION-based injection
+ "access') = 'public' OR '1'='1' OR json_extract(value, '$.", # Complex injection
+ "'; DROP TABLE checkpoints; --", # Destructive injection
+ ]
+
+ for malicious_key in malicious_keys:
+ with pytest.raises(ValueError, match="Invalid filter key"):
+ _metadata_predicate({malicious_key: "dummy"})
+
+ def test_checkpoint_search_sql_injection_prevention(self) -> None:
+ """Test that SQL injection via malicious filter keys is prevented in checkpoint search."""
+ with SqliteSaver.from_conn_string(":memory:") as saver:
+ # Setup: Create checkpoints with different metadata
+ config_public: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-public",
+ "checkpoint_ns": "",
+ }
+ }
+ config_private: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-private",
+ "checkpoint_ns": "",
+ }
+ }
+
+ checkpoint_public = empty_checkpoint()
+ checkpoint_private = empty_checkpoint()
+
+ metadata_public: CheckpointMetadata = {
+ "access": "public",
+ "data": "public information",
+ }
+ metadata_private: CheckpointMetadata = {
+ "access": "private",
+ "data": "secret information",
+ "password": "secret123",
+ }
+
+ saver.put(config_public, checkpoint_public, metadata_public, {})
+ saver.put(config_private, checkpoint_private, metadata_private, {})
+
+ # Normal query - should return only public checkpoint
+ normal_results = list(saver.list(None, filter={"access": "public"}))
+ assert len(normal_results) == 1
+ assert normal_results[0].metadata["access"] == "public"
+
+ # SQL injection attempt should raise ValueError
+ malicious_key = (
+ "access') = 'public' OR '1'='1' OR json_extract(metadata, '$."
+ )
+
+ with pytest.raises(ValueError, match="Invalid filter key"):
+ list(saver.list(None, filter={malicious_key: "dummy"}))
+
+ def test_limit_parameter_sql_injection_prevention(self) -> None:
+ """Test that the limit parameter properly uses parameterized queries to prevent SQL injection."""
+ with SqliteSaver.from_conn_string(":memory:") as saver:
+ # Setup: Create multiple checkpoints
+ for i in range(5):
+ config: RunnableConfig = {
+ "configurable": {
+ "thread_id": f"thread-{i}",
+ "checkpoint_ns": "",
+ }
+ }
+ checkpoint = empty_checkpoint()
+ metadata: CheckpointMetadata = {"index": i}
+ saver.put(config, checkpoint, metadata, {})
+
+ # Test that limit works correctly with valid integer
+ results = list(saver.list(None, limit=2))
+ assert len(results) == 2
+
+ # Test that limit=0 returns no results
+ results = list(saver.list(None, limit=0))
+ assert len(results) == 0
+
+ # Test that limit=None returns all results
+ results = list(saver.list(None, limit=None))
+ assert len(results) == 5
+
+ def test_metadata_filter_keys_with_hyphens_and_digits(self) -> None:
+ """Metadata keys with hyphens and digit-start should be filterable.
+
+ This exposes incorrect JSON path handling (unquoted segments) by asserting
+ that such filters successfully match saved checkpoints.
+ """
+ with SqliteSaver.from_conn_string(":memory:") as saver:
+ config: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-hyphen-digit",
+ "checkpoint_ns": "",
+ }
+ }
+ checkpoint = empty_checkpoint()
+ metadata: CheckpointMetadata = {
+ "access-level": "public",
+ "user": {"access-level": "nested", "123abc": "ok2"},
+ "123abc": "ok",
+ }
+ saver.put(config, checkpoint, metadata, {})
+
+ # Top-level hyphenated key
+ results = list(saver.list(None, filter={"access-level": "public"}))
+ assert len(results) == 1
+
+ # Nested hyphenated key via dotted path
+ results = list(saver.list(None, filter={"user.access-level": "nested"}))
+ assert len(results) == 1
+
+ # Top-level digit-starting key
+ results = list(saver.list(None, filter={"123abc": "ok"}))
+ assert len(results) == 1
+
+ # Nested digit-starting key via dotted path
+ results = list(saver.list(None, filter={"user.123abc": "ok2"}))
+ assert len(results) == 1
diff --git a/langgraph/source/libs/checkpoint-sqlite/tests/test_store.py b/langgraph/source/libs/checkpoint-sqlite/tests/test_store.py
new file mode 100644
index 0000000000000000000000000000000000000000..444c27e544f19d15cf8cc2fd672e066ec74e3cad
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/tests/test_store.py
@@ -0,0 +1,1232 @@
+# mypy: disable-error-code="union-attr,arg-type,index,operator"
+import os
+import re
+import tempfile
+import uuid
+from collections.abc import Generator, Iterable
+from contextlib import contextmanager
+from typing import Any, Literal, cast
+
+import pytest
+from langchain_core.embeddings import Embeddings
+from langgraph.store.base import (
+ GetOp,
+ Item,
+ ListNamespacesOp,
+ MatchCondition,
+ PutOp,
+ SearchOp,
+)
+
+from langgraph.store.sqlite import SqliteStore
+from langgraph.store.sqlite.base import SqliteIndexConfig
+
+
+# Local embeddings implementation for testing vector search
+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."""
+ import math
+ import random
+ from collections import defaultdict
+
+ self._rng = random.Random(seed)
+ self.dims = dims
+ # Create projection vector for each character lazily
+ self._char_projections: dict[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."""
+ import math
+ from collections import Counter
+
+ 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
+
+
+@pytest.fixture(scope="function", params=["memory", "file"])
+def store(request: Any) -> Generator[SqliteStore, None, None]:
+ """Create a SqliteStore for testing."""
+ if request.param == "memory":
+ # In-memory store
+ with SqliteStore.from_conn_string(":memory:") as store:
+ store.setup()
+ yield store
+ else:
+ # Temporary file store
+ temp_file = tempfile.NamedTemporaryFile(delete=False)
+ temp_file.close()
+ try:
+ with SqliteStore.from_conn_string(temp_file.name) as store:
+ store.setup()
+ yield store
+ finally:
+ os.unlink(temp_file.name)
+
+
+@pytest.fixture(scope="function")
+def fake_embeddings() -> CharacterEmbeddings:
+ """Create fake embeddings for testing."""
+ return CharacterEmbeddings(dims=500)
+
+
+# Define vector types and distance types for parametrized tests
+VECTOR_TYPES = ["cosine"] # SQLite only supports cosine similarity
+
+
+@contextmanager
+def create_vector_store(
+ fake_embeddings: CharacterEmbeddings,
+ text_fields: list[str] | None = None,
+ distance_type: str = "cosine",
+ conn_type: Literal["memory", "file"] = "memory",
+) -> Generator[SqliteStore, None, None]:
+ """Create a SqliteStore with vector search enabled."""
+ index_config: SqliteIndexConfig = {
+ "dims": fake_embeddings.dims,
+ "embed": fake_embeddings,
+ "text_fields": text_fields,
+ "distance_type": distance_type, # This is for API consistency but SQLite only supports cosine
+ }
+ if conn_type == "memory":
+ conn_str = ":memory:"
+ else:
+ temp_file = tempfile.NamedTemporaryFile(delete=False)
+ temp_file.close()
+ conn_str = temp_file.name
+
+ try:
+ with SqliteStore.from_conn_string(conn_str, index=index_config) as store:
+ store.setup()
+ yield store
+ finally:
+ if conn_type == "file":
+ os.unlink(conn_str)
+
+
+def test_batch_order(store: SqliteStore) -> None:
+ # Setup test data
+ store.put(("test", "foo"), "key1", {"data": "value1"})
+ store.put(("test", "bar"), "key2", {"data": "value2"})
+
+ ops = [
+ 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(
+ cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
+ )
+ 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[0].namespace == ("test", "foo")
+ assert results[1] is None # Put operation returns None
+ assert isinstance(results[2], list)
+ assert len(results[2]) == 1
+ assert results[2][0].key == "key1"
+ assert results[2][0].value == {"data": "value1"}
+ assert isinstance(results[3], list)
+ assert len(results[3]) > 0 # Should contain at least our test namespaces
+ assert ("test", "foo") in results[3]
+ assert ("test", "bar") in results[3]
+ 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", "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", "foo"), key="key1"),
+ ]
+
+ results_reordered = store.batch(
+ cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops_reordered)
+ )
+ assert len(results_reordered) == 5
+ assert isinstance(results_reordered[0], list)
+ 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 results_reordered[1].namespace == ("test", "bar")
+ assert isinstance(results_reordered[2], list)
+ 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"
+ assert results_reordered[4].namespace == ("test", "foo")
+
+ # Verify the put worked
+ item3 = store.get(("test",), "key3")
+ assert item3 is not None
+ assert item3.value == {"data": "value3"}
+
+
+def test_batch_get_ops(store: SqliteStore) -> None:
+ # 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"), # Non-existent key
+ ]
+
+ results = store.batch(ops)
+
+ assert len(results) == 3
+ assert results[0] is not None
+ assert results[1] is not None
+ assert results[2] is None
+ assert results[0].key == "key1"
+ assert results[1].key == "key2"
+
+
+def test_batch_put_ops(store: SqliteStore) -> None:
+ ops = [
+ PutOp(namespace=("test",), key="key1", value={"data": "value1"}),
+ PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
+ 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)
+
+ # 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: SqliteStore) -> None:
+ # 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={"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
+
+ # 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: SqliteStore) -> None:
+ # 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),
+ ListNamespacesOp(match_conditions=None, max_depth=2, limit=10, offset=0),
+ ListNamespacesOp(
+ match_conditions=tuple([MatchCondition("suffix", ("public",))]),
+ max_depth=None,
+ limit=10,
+ offset=0,
+ ),
+ ]
+
+ results = store.batch(
+ cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
+ )
+ assert len(results) == 3
+
+ # 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 TestSqliteStore:
+ def test_basic_store_ops(self) -> None:
+ with SqliteStore.from_conn_string(":memory:") as store:
+ store.setup()
+ namespace = ("test", "documents")
+ item_id = "doc1"
+ item_value = {"title": "Test Document", "content": "Hello, World!"}
+
+ store.put(namespace, item_id, item_value)
+ item = store.get(namespace, item_id)
+
+ assert item
+ assert item.namespace == namespace
+ assert item.key == item_id
+ assert item.value == item_value
+
+ # Test update
+ # Small delay to ensure the updated timestamp is different
+ import time
+
+ time.sleep(0.01)
+
+ 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
+ # Don't check timestamps because SQLite execution might be too fast
+ # 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
+
+ # Test delete
+ store.delete(namespace, item_id)
+ deleted_item = store.get(namespace, item_id)
+ assert deleted_item is None
+
+ def test_list_namespaces(self) -> None:
+ with SqliteStore.from_conn_string(":memory:") as store:
+ store.setup()
+ # Create test data with various namespaces
+ test_namespaces = [
+ ("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"})
+
+ # Test listing with various filters
+ all_namespaces = store.list_namespaces()
+ assert len(all_namespaces) == len(test_namespaces)
+
+ # 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)
+
+ # 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)
+
+ # Test max depth
+ depth_2_namespaces = store.list_namespaces(max_depth=2)
+ assert all(len(ns) <= 2 for ns in depth_2_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) -> None:
+ with SqliteStore.from_conn_string(":memory:") as store:
+ store.setup()
+ # 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, key, value in test_data:
+ store.put(namespace, key, value)
+
+ # Test basic search
+ all_items = store.search(["test"])
+ assert len(all_items) == 3
+
+ # 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)
+
+ # 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)
+
+ # Test pagination
+ paginated_items = store.search(["test"], limit=2)
+ assert len(paginated_items) == 2
+
+ offset_items = store.search(["test"], offset=2)
+ assert len(offset_items) == 1
+
+ # Cleanup
+ for namespace, key, _ in test_data:
+ store.delete(namespace, key)
+
+
+def test_vector_store_initialization(fake_embeddings: CharacterEmbeddings) -> None:
+ """Test store initialization with embedding config."""
+ # Basic initialization
+ with create_vector_store(fake_embeddings) as store:
+ assert store.index_config is not None
+ assert store.embeddings == fake_embeddings
+ assert store.index_config["dims"] == fake_embeddings.dims
+ assert store.index_config.get("text_fields") is None
+
+ # With text fields specified
+ text_fields = ["content", "title"]
+ with create_vector_store(fake_embeddings, text_fields=text_fields) as store:
+ assert store.index_config is not None
+ assert store.embeddings == fake_embeddings
+ assert store.index_config["dims"] == fake_embeddings.dims
+ assert store.index_config["text_fields"] == text_fields
+
+ # Ensure store setup properly creates the vector tables
+ with create_vector_store(fake_embeddings) as store:
+ # Check if vector tables exist
+ cursor = store.conn.cursor()
+ cursor.execute(
+ "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%vector%'"
+ )
+ tables = cursor.fetchall()
+ assert len(tables) >= 1, "Vector tables were not created"
+
+
+@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
+@pytest.mark.parametrize("conn_type", ["memory", "file"])
+def test_vector_insert_with_auto_embedding(
+ fake_embeddings: CharacterEmbeddings,
+ distance_type: str,
+ conn_type: Literal["memory", "file"],
+) -> None:
+ """Test inserting items that get auto-embedded."""
+ with create_vector_store(
+ fake_embeddings, distance_type=distance_type, conn_type=conn_type
+ ) as store:
+ 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
+
+
+@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
+@pytest.mark.parametrize("conn_type", ["memory", "file"])
+def test_vector_update_with_embedding(
+ fake_embeddings: CharacterEmbeddings,
+ distance_type: str,
+ conn_type: Literal["memory", "file"],
+) -> None:
+ """Test that updating items properly updates their embeddings."""
+ with create_vector_store(
+ fake_embeddings, distance_type=distance_type, conn_type=conn_type
+ ) as store:
+ 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
+
+ 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 < 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)
+
+
+@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
+def test_vector_search_with_filters(
+ fake_embeddings: CharacterEmbeddings,
+ distance_type: str,
+) -> None:
+ """Test combining vector search with filters."""
+ with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
+ # 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:
+ store.put(("test",), key, value)
+
+ results = store.search(("test",), query="apple", filter={"color": "red"})
+
+ # Check ordering and score - verify "doc1" is first result
+ assert len(results) == 2
+ assert results[0].key == "doc1"
+
+ results = store.search(("test",), query="car", filter={"color": "red"})
+ # Check ordering - verify "doc2" is first result
+ assert len(results) > 0
+ assert results[0].key == "doc2"
+
+ results = store.search(
+ ("test",), query="bbbbluuu", filter={"score": {"$gt": 3.2}}
+ )
+ # There should be 3 documents with score > 3.2
+ assert len(results) == 3
+ # Check that the blue car is the most similar to "bbbbluuu" query
+ assert results[0].key == "doc4" # The blue car should be the most relevant
+ # Verify remaining docs are ordered by appropriate similarity
+ high_score_keys = [r.key for r in results]
+ assert "doc1" in high_score_keys # score 4.5
+ assert "doc3" in high_score_keys # score 4.0
+
+ # Multiple filters
+ results = store.search(
+ ("test",), query="apple", filter={"score": {"$gte": 4.0}, "color": "green"}
+ )
+ # Check that doc3 is the top result
+ assert len(results) > 0
+ assert results[0].key == "doc3"
+
+
+@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
+def test_vector_search_pagination(
+ fake_embeddings: CharacterEmbeddings,
+ distance_type: str,
+) -> None:
+ """Test pagination with vector search."""
+ with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
+ # Insert multiple similar documents
+ for i in range(5):
+ store.put(("test",), f"doc{i}", {"text": f"test document number {i}"})
+
+ # Test with different page sizes
+ 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
+ # Make sure different pages have different results
+ assert results_page1[0].key != results_page2[0].key
+ assert results_page1[1].key != results_page2[0].key
+ assert results_page1[0].key != results_page2[1].key
+ assert results_page1[1].key != results_page2[1].key
+
+ # Check scores are in descending order within each page
+ assert results_page1[0].score >= results_page1[1].score
+ assert results_page2[0].score >= results_page2[1].score
+
+ # First page results should have higher scores than second page
+ all_results = store.search(("test",), query="test", limit=10)
+ assert len(all_results) == 5
+ assert (
+ all_results[0].score >= all_results[2].score
+ ) # First page vs second page start
+
+
+@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
+def test_vector_search_edge_cases(
+ fake_embeddings: CharacterEmbeddings,
+ distance_type: str,
+) -> None:
+ """Test edge cases in vector search."""
+ with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
+ store.put(("test",), "doc1", {"text": "test document"})
+
+ results = store.search(("test",), query="")
+ assert len(results) == 1
+
+ results = store.search(("test",), query=None)
+ assert len(results) == 1
+
+ long_query = "test " * 100
+ results = store.search(("test",), query=long_query)
+ assert len(results) == 1
+
+ special_query = "test!@#$%^&*()"
+ results = store.search(("test",), query=special_query)
+ assert len(results) == 1
+
+
+@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
+def test_embed_with_path(
+ fake_embeddings: CharacterEmbeddings,
+ distance_type: str,
+) -> None:
+ """Test vector search with specific text fields in SQLite store."""
+ with create_vector_store(
+ fake_embeddings,
+ text_fields=["key0", "key1", "key3"],
+ distance_type=distance_type,
+ ) 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
+ assert results[0].score > 0.9
+ assert results[1].score > 0.9
+
+ # ~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
+
+ # ~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
+
+ # 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 < 0.9
+ assert results[1].score < 0.9
+
+
+@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
+def test_embed_with_path_operation_config(
+ fake_embeddings: CharacterEmbeddings,
+ distance_type: str,
+) -> None:
+ """Test operation-level field configuration for vector search."""
+ with create_vector_store(
+ fake_embeddings, text_fields=["key17"], distance_type=distance_type
+ ) 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 abs(results[0].score - results[1].score) < 0.1 # Similar scores
+
+ 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 any(r.key == "doc5" for r in results)
+
+
+# Helper functions for vector similarity calculations
+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, strict=False))
+ 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
+
+
+@pytest.mark.parametrize("query", ["aaa", "bbb", "ccc", "abcd", "poisson"])
+@pytest.mark.parametrize("conn_type", ["memory", "file"])
+def test_scores(
+ fake_embeddings: CharacterEmbeddings,
+ query: str,
+ conn_type: Literal["memory", "file"],
+) -> None:
+ """Test operation-level field configuration for vector search."""
+ with create_vector_store(
+ fake_embeddings,
+ text_fields=["key0"],
+ distance_type="cosine",
+ conn_type=conn_type,
+ ) 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)
+
+ # SQLite uses cosine similarity by default
+ similarities = _cosine_similarity(vec1, [vec0])
+
+ assert len(results) == 1
+ assert results[0].score == pytest.approx(similarities[0], abs=1e-3)
+
+
+def test_nonnull_migrations() -> None:
+ """Test that all migration statements are non-null."""
+ _leading_comment_remover = re.compile(r"^/\*.*?\*/")
+ for migration in SqliteStore.MIGRATIONS:
+ statement = _leading_comment_remover.sub("", migration).split()[0]
+ assert statement.strip(), f"Empty migration statement found: {migration}"
+
+
+def test_basic_store_operations(
+ fake_embeddings: CharacterEmbeddings,
+) -> None:
+ """Test basic store operations with SQLite store."""
+ with create_vector_store(
+ fake_embeddings, text_fields=["key0", "key1", "key3"]
+ ) as store:
+ uid = uuid.uuid4().hex
+ namespace = (uid, "test", "documents")
+ item_id = "doc1"
+ item_value = {"title": "Test Document", "content": "Hello, World!"}
+ results = store.search((uid,))
+ assert len(results) == 0
+
+ store.put(namespace, item_id, item_value)
+ item = store.get(namespace, item_id)
+
+ assert item is not None
+ assert item.namespace == namespace
+ assert item.key == item_id
+ assert item.value == item_value
+ assert item.created_at is not None
+ assert item.updated_at is not None
+
+ updated_value = {
+ "title": "Updated Test Document",
+ "content": "Hello, LangGraph!",
+ }
+ store.put(namespace, item_id, updated_value)
+ updated_item = store.get(namespace, item_id)
+ assert updated_item is not None
+
+ assert updated_item.value == updated_value
+ assert updated_item.updated_at >= item.updated_at
+
+ different_namespace = (uid, "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)
+
+ items = store.search((uid, "test"), limit=10)
+ 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=(uid, "test"))
+ assert (uid, "test", "documents") in namespaces
+
+ 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((uid, "test"), limit=10)
+ assert len(empty_search_results) == 0
+
+
+def test_list_namespaces_operations(
+ fake_embeddings: CharacterEmbeddings,
+) -> None:
+ """Test list namespaces functionality with various filters."""
+ with create_vector_store(
+ fake_embeddings, text_fields=["key0", "key1", "key3"]
+ ) 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),
+ ]
+
+ # Add test data
+ for namespace in test_namespaces:
+ store.put(namespace, "dummy", {"content": "dummy"})
+
+ # Test prefix filtering
+ 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 specific prefix
+ 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 suffix filtering
+ 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 combined prefix and suffix
+ 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 wildcard in prefix
+ 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)
+
+ # Test wildcard in suffix
+ 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,
+ )
+
+ # Test max depth
+ 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(res for res in max_depth_result)) == len(max_depth_result) == 5
+
+ # Test pagination
+ 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(empty_prefix_result) == set(test_namespaces)
+
+ # Clean up
+ for namespace in test_namespaces:
+ store.delete(namespace, "dummy")
+
+
+def test_search_items(
+ fake_embeddings: CharacterEmbeddings,
+) -> None:
+ """Test search_items functionality by calling store methods directly."""
+ base = "test_search_items"
+ test_namespaces = [
+ (base, "documents", "user1"),
+ (base, "documents", "user2"),
+ (base, "reports", "department1"),
+ (base, "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"]},
+ ]
+
+ with create_vector_store(
+ fake_embeddings, text_fields=["key0", "key1", "key3"]
+ ) as store:
+ # Insert test data
+ for ns, item in zip(test_namespaces, test_items, strict=False):
+ key = f"item_{ns[-1]}"
+ store.put(ns, key, item)
+
+ # 1. Search documents
+ docs = store.search((base, "documents"))
+ assert len(docs) == 2
+ assert all(item.namespace[1] == "documents" for item in docs)
+
+ # 2. Search reports
+ reports = store.search((base, "reports"))
+ assert len(reports) == 2
+ assert all(item.namespace[1] == "reports" for item in reports)
+
+ # 3. Pagination
+ first_page = store.search((base,), limit=2, offset=0)
+ second_page = store.search((base,), limit=2, offset=2)
+ assert len(first_page) == 2
+ assert len(second_page) == 2
+ keys_page1 = {item.key for item in first_page}
+ keys_page2 = {item.key for item in second_page}
+ assert keys_page1.isdisjoint(keys_page2)
+ all_items = store.search((base,))
+ assert len(all_items) == 4
+
+ john_items = store.search((base,), filter={"author": "John Doe"})
+ assert len(john_items) == 2
+ assert all(item.value["author"] == "John Doe" for item in john_items)
+
+ draft_items = store.search((base,), filter={"tags": ["draft"]})
+ assert len(draft_items) == 2
+ assert all("draft" in item.value["tags"] for item in draft_items)
+
+ for ns in test_namespaces:
+ key = f"item_{ns[-1]}"
+ store.delete(ns, key)
+
+
+def test_sql_injection_vulnerability(store: SqliteStore) -> None:
+ """Test that SQL injection via malicious filter keys is prevented."""
+ # Add public and private documents
+ store.put(("docs",), "public", {"access": "public", "data": "public info"})
+ store.put(
+ ("docs",), "private", {"access": "private", "data": "secret", "password": "123"}
+ )
+
+ # Normal query - returns 1 public document
+ normal = store.search(("docs",), filter={"access": "public"})
+ assert len(normal) == 1
+ assert normal[0].value["access"] == "public"
+
+ # SQL injection attempt via malicious key should raise ValueError
+ malicious_key = "access') = 'public' OR '1'='1' OR json_extract(value, '$."
+
+ with pytest.raises(ValueError, match="Invalid filter key"):
+ store.search(("docs",), filter={malicious_key: "dummy"})
+
+
+def test_sql_injection_filter_values(store: SqliteStore) -> None:
+ """Test that SQL injection via malicious filter values is properly escaped."""
+ # Setup: Create documents with different access levels
+ store.put(("docs",), "doc1", {"access": "public", "title": "Public Document"})
+ store.put(("docs",), "doc2", {"access": "private", "title": "Private Document"})
+ store.put(("docs",), "doc3", {"access": "secret", "title": "Secret Document"})
+
+ # Test 1: Basic SQL injection attempt with single quote
+ malicious_value = "public' OR '1'='1"
+ results = store.search(("docs",), filter={"access": malicious_value})
+ # Should return 0 results because the malicious value is escaped and won't match anything
+ assert len(results) == 0, "SQL injection via string value should be blocked"
+
+ # Test 2: SQL injection with comment
+ malicious_value = "public'; --"
+ results = store.search(("docs",), filter={"access": malicious_value})
+ assert len(results) == 0, "SQL comment injection should be blocked"
+
+ # Test 3: UNION injection attempt
+ malicious_value = "public' UNION SELECT * FROM store --"
+ results = store.search(("docs",), filter={"access": malicious_value})
+ assert len(results) == 0, "UNION injection should be blocked"
+
+ # Test 4: Parameterized queries handle strings with null bytes and SQL injection attempts safely
+ malicious_value = "public\x00' OR '1'='1"
+ results = store.search(("docs",), filter={"access": malicious_value})
+ assert len(results) == 0, (
+ "Parameterized queries treat injection attempts as literal strings"
+ )
+
+ # Test 5: Multiple single quotes
+ malicious_value = "''''"
+ results = store.search(("docs",), filter={"access": malicious_value})
+ assert len(results) == 0, "Multiple quotes should be handled safely"
+
+ # Test 6: Legitimate value with single quote should work
+ store.put(("docs",), "doc4", {"title": "O'Brien's Document", "access": "public"})
+ results = store.search(("docs",), filter={"title": "O'Brien's Document"})
+ assert len(results) == 1, "Legitimate single quotes should work"
+ assert results[0].value["title"] == "O'Brien's Document"
+
+ # Test 7: Unicode characters with injection attempt
+ malicious_value = "public' OR 'א'='א"
+ results = store.search(("docs",), filter={"access": malicious_value})
+ assert len(results) == 0, "Unicode-based injection should be blocked"
+
+
+def test_numeric_filter_safety(store: SqliteStore) -> None:
+ """Test that numeric filter values are handled safely."""
+ # Setup: Create documents with numeric fields
+ store.put(("items",), "item1", {"price": 10, "quantity": 5})
+ store.put(("items",), "item2", {"price": 20, "quantity": 3})
+ store.put(("items",), "item3", {"price": 30, "quantity": 1})
+
+ # Test 1: Normal numeric comparison
+ results = store.search(("items",), filter={"price": {"$gt": 15}})
+ assert len(results) == 2
+ assert all(r.value["price"] > 15 for r in results)
+
+ # Test 2: Special float values (infinity)
+ results = store.search(("items",), filter={"price": {"$lt": float("inf")}})
+ assert len(results) == 3, "All finite values should be less than infinity"
+
+ # Test 3: Special float values (negative infinity)
+ results = store.search(("items",), filter={"price": {"$gt": float("-inf")}})
+ assert len(results) == 3, (
+ "All finite values should be greater than negative infinity"
+ )
+
+ # Test 4: NaN handling - NaN comparisons should not cause errors
+ try:
+ results = store.search(("items",), filter={"price": {"$eq": float("nan")}})
+ # NaN never equals anything, including itself, so should return 0 results
+ assert len(results) == 0
+ except Exception as e:
+ pytest.fail(f"NaN handling should not raise exception: {e}")
+
+ # Test 5: Very large numbers
+ results = store.search(("items",), filter={"price": {"$lt": 10**100}})
+ assert len(results) == 3, "Very large numbers should be handled safely"
+
+ # Test 6: Negative numbers
+ store.put(("items",), "item4", {"price": -10, "quantity": 0})
+ results = store.search(("items",), filter={"price": {"$lt": 0}})
+ assert len(results) == 1
+ assert results[0].key == "item4"
+
+
+def test_boolean_filter_safety(store: SqliteStore) -> None:
+ """Test that boolean filter values are handled safely."""
+ store.put(("flags",), "flag1", {"active": True, "name": "Feature A"})
+ store.put(("flags",), "flag2", {"active": False, "name": "Feature B"})
+ store.put(("flags",), "flag3", {"active": True, "name": "Feature C"})
+
+ # Test boolean filters
+ results = store.search(("flags",), filter={"active": True})
+ assert len(results) == 2
+ assert all(r.value["active"] is True for r in results)
+
+ results = store.search(("flags",), filter={"active": False})
+ assert len(results) == 1
+ assert results[0].value["active"] is False
+
+
+def test_filter_keys_with_hyphens_and_digits(store: SqliteStore) -> None:
+ """Keys with hyphens or leading digits should be queryable via filters.
+
+ Current unquoted JSON path construction (e.g., '$.access-level' or '$.123abc')
+ is not valid JSON1 syntax, so this test will catch regressions in path handling.
+ """
+ # Documents with top-level and nested keys requiring bracket-quoted JSON paths
+ store.put(
+ ("docs",),
+ "hyphen",
+ {"access-level": "public", "user": {"access-level": "nested"}},
+ )
+ store.put(("docs",), "digit", {"123abc": "ok", "user": {"123abc": "ok2"}})
+
+ # Top-level hyphenated key
+ results = store.search(("docs",), filter={"access-level": "public"})
+ assert [r.key for r in results] == ["hyphen"]
+
+ # Nested hyphenated key via dotted path
+ results = store.search(("docs",), filter={"user.access-level": "nested"})
+ assert [r.key for r in results] == ["hyphen"]
+
+ # Top-level digit-starting key
+ results = store.search(("docs",), filter={"123abc": "ok"})
+ assert [r.key for r in results] == ["digit"]
+
+ # Nested digit-starting key via dotted path
+ results = store.search(("docs",), filter={"user.123abc": "ok2"})
+ assert [r.key for r in results] == ["digit"]
+
+
+@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
+def test_non_ascii(
+ fake_embeddings: CharacterEmbeddings,
+ distance_type: str,
+) -> None:
+ """Test support for non-ascii characters"""
+ with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
+ store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
+ store.put(
+ ("user_123", "memories"), "2", {"text": "これは日本語です"}
+ ) # Japanese
+ store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
+ store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
+ store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
+
+ result1 = store.search(("user_123", "memories"), query="这是中文")
+ result2 = store.search(("user_123", "memories"), query="これは日本語です")
+ result3 = store.search(("user_123", "memories"), query="이건 한국어야")
+ result4 = store.search(("user_123", "memories"), query="Это русский")
+ result5 = store.search(("user_123", "memories"), query="यह रूसी है")
+
+ assert result1[0].key == "1"
+ assert result2[0].key == "2"
+ assert result3[0].key == "3"
+ assert result4[0].key == "4"
+ assert result5[0].key == "5"
diff --git a/langgraph/source/libs/checkpoint-sqlite/tests/test_ttl.py b/langgraph/source/libs/checkpoint-sqlite/tests/test_ttl.py
new file mode 100644
index 0000000000000000000000000000000000000000..28d43a790517efdfe02d67773b1cc0f2de110a9f
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/tests/test_ttl.py
@@ -0,0 +1,429 @@
+"""Test SQLite store Time-To-Live (TTL) functionality."""
+
+import asyncio
+import os
+import tempfile
+import time
+from collections.abc import Generator
+
+import pytest
+from langgraph.store.base import TTLConfig
+
+from langgraph.store.sqlite import SqliteStore
+from langgraph.store.sqlite.aio import AsyncSqliteStore
+
+
+@pytest.fixture
+def temp_db_file() -> Generator[str, None, None]:
+ """Create a temporary database file for testing."""
+ fd, path = tempfile.mkstemp()
+ os.close(fd)
+ yield path
+ os.unlink(path)
+
+
+def test_ttl_basic(temp_db_file: str) -> None:
+ """Test basic TTL functionality with synchronous API."""
+ ttl_seconds = 1
+ ttl_minutes = ttl_seconds / 60
+
+ with SqliteStore.from_conn_string(
+ temp_db_file, ttl={"default_ttl": ttl_minutes}
+ ) as store:
+ store.setup()
+
+ store.put(("test",), "item1", {"value": "test"})
+
+ item = store.get(("test",), "item1")
+ assert item is not None
+ assert item.value["value"] == "test"
+
+ time.sleep(ttl_seconds + 1.0)
+
+ store.sweep_ttl()
+
+ item = store.get(("test",), "item1")
+ assert item is None
+
+
+@pytest.mark.flaky(retries=3)
+def test_ttl_refresh(temp_db_file: str) -> None:
+ """Test TTL refresh on read."""
+ ttl_seconds = 1
+ ttl_minutes = ttl_seconds / 60
+
+ with SqliteStore.from_conn_string(
+ temp_db_file, ttl={"default_ttl": ttl_minutes, "refresh_on_read": True}
+ ) as store:
+ store.setup()
+
+ # Store an item with TTL
+ store.put(("test",), "item1", {"value": "test"})
+
+ # Sleep almost to expiration
+ time.sleep(ttl_seconds - 0.5)
+ swept = store.sweep_ttl()
+ assert swept == 0
+
+ # Get the item and refresh TTL
+ item = store.get(("test",), "item1", refresh_ttl=True)
+ assert item is not None
+
+ time.sleep(ttl_seconds - 0.5)
+ swept = store.sweep_ttl()
+ assert swept == 0
+
+ # Get the item, should still be there
+ item = store.get(("test",), "item1")
+ assert item is not None
+ assert item.value["value"] == "test"
+
+ # Sleep again but don't refresh this time
+ time.sleep(ttl_seconds + 0.75)
+
+ swept = store.sweep_ttl()
+ assert swept == 1
+
+ # Item should be gone now
+ item = store.get(("test",), "item1")
+ assert item is None
+
+
+def test_ttl_sweeper(temp_db_file: str) -> None:
+ """Test TTL sweeper thread."""
+ ttl_seconds = 2
+ ttl_minutes = ttl_seconds / 60
+
+ ttl_config: TTLConfig = {
+ "default_ttl": ttl_minutes,
+ "sweep_interval_minutes": ttl_minutes / 2,
+ }
+ with SqliteStore.from_conn_string(
+ temp_db_file,
+ ttl=ttl_config,
+ ) as store:
+ store.setup()
+
+ # Start the TTL sweeper
+ store.start_ttl_sweeper()
+
+ # Store an item with TTL
+ store.put(("test",), "item1", {"value": "test"})
+
+ # Item should be there initially
+ item = store.get(("test",), "item1")
+ assert item is not None
+
+ # Wait for TTL to expire and the sweeper to run
+ time.sleep(ttl_seconds + (ttl_seconds / 2) + 0.5)
+
+ # Item should be gone now (swept automatically)
+ item = store.get(("test",), "item1")
+ assert item is None
+
+ # Stop the sweeper
+ store.stop_ttl_sweeper()
+
+
+@pytest.mark.flaky(retries=3)
+def test_ttl_custom_value(temp_db_file: str) -> None:
+ """Test TTL with custom value per item."""
+ with SqliteStore.from_conn_string(temp_db_file) as store:
+ store.setup()
+
+ # Store items with different TTLs
+ store.put(("test",), "item1", {"value": "short"}, ttl=1 / 60) # 1 second
+ store.put(("test",), "item2", {"value": "long"}, ttl=3 / 60) # 3 seconds
+
+ # Item with short TTL
+ time.sleep(2) # Wait for short TTL
+ store.sweep_ttl()
+
+ # Short TTL item should be gone, long TTL item should remain
+ item1 = store.get(("test",), "item1")
+ item2 = store.get(("test",), "item2")
+ assert item1 is None
+ assert item2 is not None
+
+ # Wait for the second item's TTL
+ time.sleep(4)
+ store.sweep_ttl()
+
+ # Now both should be gone
+ item2 = store.get(("test",), "item2")
+ assert item2 is None
+
+
+@pytest.mark.flaky(retries=3)
+def test_ttl_override_default(temp_db_file: str) -> None:
+ """Test overriding default TTL at the item level."""
+ with SqliteStore.from_conn_string(
+ temp_db_file,
+ ttl={"default_ttl": 5 / 60}, # 5 seconds default
+ ) as store:
+ store.setup()
+
+ # Store an item with shorter than default TTL
+ store.put(("test",), "item1", {"value": "override"}, ttl=1 / 60) # 1 second
+
+ # Store an item with default TTL
+ store.put(("test",), "item2", {"value": "default"}) # Uses default 5 seconds
+
+ # Store an item with no TTL
+ store.put(("test",), "item3", {"value": "permanent"}, ttl=None)
+
+ # Wait for the override TTL to expire
+ time.sleep(2)
+ store.sweep_ttl()
+
+ # Check results
+ item1 = store.get(("test",), "item1")
+ item2 = store.get(("test",), "item2")
+ item3 = store.get(("test",), "item3")
+
+ assert item1 is None # Should be expired
+ assert item2 is not None # Default TTL, should still be there
+ assert item3 is not None # No TTL, should still be there
+
+ # Wait for default TTL to expire
+ time.sleep(4)
+ store.sweep_ttl()
+
+ # Check results again
+ item2 = store.get(("test",), "item2")
+ item3 = store.get(("test",), "item3")
+
+ assert item2 is None # Default TTL item should be gone
+ assert item3 is not None # No TTL item should still be there
+
+
+@pytest.mark.flaky(retries=3)
+def test_search_with_ttl(temp_db_file: str) -> None:
+ """Test TTL with search operations."""
+ ttl_seconds = 1
+ ttl_minutes = ttl_seconds / 60
+
+ with SqliteStore.from_conn_string(
+ temp_db_file, ttl={"default_ttl": ttl_minutes}
+ ) as store:
+ store.setup()
+
+ # Store items
+ store.put(("test",), "item1", {"value": "apple"})
+ store.put(("test",), "item2", {"value": "banana"})
+
+ # Search before expiration
+ results = store.search(("test",), filter={"value": "apple"})
+ assert len(results) == 1
+ assert results[0].key == "item1"
+
+ # Wait for TTL to expire
+ time.sleep(ttl_seconds + 1)
+ store.sweep_ttl()
+
+ # Search after expiration
+ results = store.search(("test",), filter={"value": "apple"})
+ assert len(results) == 0
+
+
+@pytest.mark.asyncio
+async def test_async_ttl_basic(temp_db_file: str) -> None:
+ """Test basic TTL functionality with asynchronous API."""
+ ttl_seconds = 1
+ ttl_minutes = ttl_seconds / 60
+
+ async with AsyncSqliteStore.from_conn_string(
+ temp_db_file, ttl={"default_ttl": ttl_minutes}
+ ) as store:
+ await store.setup()
+
+ # Store an item with TTL
+ await store.aput(("test",), "item1", {"value": "test"})
+
+ # Get the item before expiration
+ item = await store.aget(("test",), "item1")
+ assert item is not None
+ assert item.value["value"] == "test"
+
+ # Wait for TTL to expire
+ await asyncio.sleep(ttl_seconds + 1.0)
+
+ # Manual sweep needed without the sweeper thread
+ await store.sweep_ttl()
+
+ # Item should be gone now
+ item = await store.aget(("test",), "item1")
+ assert item is None
+
+
+@pytest.mark.asyncio
+@pytest.mark.flaky(retries=3)
+async def test_async_ttl_refresh(temp_db_file: str) -> None:
+ """Test TTL refresh on read with async API."""
+ ttl_seconds = 1
+ ttl_minutes = ttl_seconds / 60
+
+ async with AsyncSqliteStore.from_conn_string(
+ temp_db_file, ttl={"default_ttl": ttl_minutes, "refresh_on_read": True}
+ ) as store:
+ await store.setup()
+
+ # Store an item with TTL
+ await store.aput(("test",), "item1", {"value": "test"})
+
+ # Sleep almost to expiration
+ await asyncio.sleep(ttl_seconds - 0.5)
+
+ # Get the item and refresh TTL
+ item = await store.aget(("test",), "item1", refresh_ttl=True)
+ assert item is not None
+
+ # Sleep again - without refresh, would have expired by now
+ await asyncio.sleep(ttl_seconds - 0.5)
+
+ # Get the item, should still be there
+ item = await store.aget(("test",), "item1")
+ assert item is not None
+ assert item.value["value"] == "test"
+
+ # Sleep again but don't refresh this time
+ await asyncio.sleep(ttl_seconds + 1.0)
+
+ # Manual sweep
+ await store.sweep_ttl()
+
+ # Item should be gone now
+ item = await store.aget(("test",), "item1")
+ assert item is None
+
+
+@pytest.mark.asyncio
+async def test_async_ttl_sweeper(temp_db_file: str) -> None:
+ """Test TTL sweeper thread with async API."""
+ ttl_seconds = 2
+ ttl_minutes = ttl_seconds / 60
+
+ ttl_config: TTLConfig = {
+ "default_ttl": ttl_minutes,
+ "sweep_interval_minutes": ttl_minutes / 2,
+ }
+
+ async with AsyncSqliteStore.from_conn_string(
+ temp_db_file,
+ ttl=ttl_config,
+ ) as store:
+ await store.setup()
+
+ # Start the TTL sweeper
+ await store.start_ttl_sweeper()
+
+ # Store an item with TTL
+ await store.aput(("test",), "item1", {"value": "test"})
+
+ # Item should be there initially
+ item = await store.aget(("test",), "item1")
+ assert item is not None
+
+ # Wait for TTL to expire and the sweeper to run
+ await asyncio.sleep(ttl_seconds + (ttl_seconds / 2) + 0.5)
+
+ # Item should be gone now (swept automatically)
+ item = await store.aget(("test",), "item1")
+ assert item is None
+
+ # Stop the sweeper
+ await store.stop_ttl_sweeper()
+
+
+@pytest.mark.asyncio
+@pytest.mark.flaky(retries=3)
+async def test_async_search_with_ttl(temp_db_file: str) -> None:
+ """Test TTL with search operations using async API."""
+ ttl_seconds = 1
+ ttl_minutes = ttl_seconds / 60
+
+ async with AsyncSqliteStore.from_conn_string(
+ temp_db_file, ttl={"default_ttl": ttl_minutes}
+ ) as store:
+ await store.setup()
+
+ # Store items
+ await store.aput(("test",), "item1", {"value": "apple"})
+ await store.aput(("test",), "item2", {"value": "banana"})
+
+ # Search before expiration
+ results = await store.asearch(("test",), filter={"value": "apple"})
+ assert len(results) == 1
+ assert results[0].key == "item1"
+
+ # Wait for TTL to expire
+ await asyncio.sleep(ttl_seconds + 1)
+ await store.sweep_ttl()
+
+ # Search after expiration
+ results = await store.asearch(("test",), filter={"value": "apple"})
+ assert len(results) == 0
+
+
+@pytest.mark.asyncio
+@pytest.mark.flaky(retries=3)
+async def test_async_asearch_refresh_ttl(temp_db_file: str) -> None:
+ """Test TTL refresh on asearch with async API."""
+ ttl_seconds = 4.0 # Increased TTL for less sensitivity to timing
+ ttl_minutes = ttl_seconds / 60.0
+
+ async with AsyncSqliteStore.from_conn_string(
+ temp_db_file, ttl={"default_ttl": ttl_minutes, "refresh_on_read": True}
+ ) as store:
+ await store.setup()
+
+ namespace = ("docs", "user1")
+ # t=0: items put, expire at t=4.0s
+ await store.aput(namespace, "item1", {"text": "content1", "id": 1})
+ await store.aput(namespace, "item2", {"text": "content2", "id": 2})
+
+ # t=3.0s: (after sleep ttl_seconds * 0.75 = 3s)
+ await asyncio.sleep(ttl_seconds * 0.75)
+
+ # Perform asearch with refresh_ttl=True for item1.
+ # item1's TTL should be refreshed. New expiry: t=3.0s + 4.0s = t=7.0s.
+ # item2's TTL is not affected. Expires at t=4.0s.
+ searched_items = await store.asearch(
+ namespace, filter={"id": 1}, refresh_ttl=True
+ )
+ assert len(searched_items) == 1
+ assert searched_items[0].key == "item1"
+
+ # t=5.0s: (after sleep ttl_seconds * 0.5 = 2s more. Total elapsed: 3s + 2s = 5s)
+ await asyncio.sleep(ttl_seconds * 0.5)
+ # At this point:
+ # - item1 (refreshed by asearch) should expire at t=7.0s. Should be ALIVE.
+ # - item2 (original TTL) should have expired at t=4.0s. Should be GONE after sweep.
+
+ await store.sweep_ttl()
+
+ # Check item1 (should exist due to asearch refresh)
+ item1_check1 = await store.aget(namespace, "item1", refresh_ttl=False)
+ assert item1_check1 is not None, (
+ "Item1 should exist after asearch refresh and first sweep"
+ )
+ assert item1_check1.value["text"] == "content1"
+
+ # Check item2 (should be gone)
+ item2_check1 = await store.aget(namespace, "item2", refresh_ttl=False)
+ assert item2_check1 is None, (
+ "Item2 should be gone after its original TTL expired"
+ )
+
+ # t=7.5s: (after sleep ttl_seconds * 0.625 = 2.5s more. Total elapsed: 5s + 2.5s = 7.5s)
+ await asyncio.sleep(ttl_seconds * 0.625)
+ # At this point:
+ # - item1 (refreshed by asearch, expired at t=7.0s) should be GONE after sweep.
+
+ await store.sweep_ttl()
+
+ # Check item1 again (should be gone now)
+ item1_final_check = await store.aget(namespace, "item1", refresh_ttl=False)
+ assert item1_final_check is None, (
+ "Item1 should be gone after its refreshed TTL expired"
+ )
diff --git a/langgraph/source/libs/checkpoint-sqlite/uv.lock b/langgraph/source/libs/checkpoint-sqlite/uv.lock
new file mode 100644
index 0000000000000000000000000000000000000000..06d542c93c5cabe9b1c88d8b2adae724045dd715
--- /dev/null
+++ b/langgraph/source/libs/checkpoint-sqlite/uv.lock
@@ -0,0 +1,1278 @@
+version = 1
+revision = 3
+requires-python = ">=3.10"
+
+[[package]]
+name = "aiosqlite"
+version = "0.22.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.12.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
+]
+
+[[package]]
+name = "backports-asyncio-runner"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.1.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
+ { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
+ { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
+ { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
+ { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
+ { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
+ { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
+ { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
+ { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
+ { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
+ { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
+ { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
+ { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
+ { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
+ { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
+ { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
+ { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
+ { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
+ { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
+]
+
+[[package]]
+name = "codespell"
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740, upload-time = "2025-01-28T18:52:39.411Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501, upload-time = "2025-01-28T18:52:37.057Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "jsonpatch"
+version = "1.33"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonpointer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" },
+]
+
+[[package]]
+name = "jsonpointer"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" },
+]
+
+[[package]]
+name = "langchain-core"
+version = "1.2.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonpatch" },
+ { name = "langsmith" },
+ { name = "packaging" },
+ { name = "pydantic" },
+ { name = "pyyaml" },
+ { name = "tenacity" },
+ { name = "typing-extensions" },
+ { name = "uuid-utils" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a2/0e/664d8d81b3493e09cbab72448d2f9d693d1fa5aa2bcc488602203a9b6da0/langchain_core-1.2.7.tar.gz", hash = "sha256:e1460639f96c352b4a41c375f25aeb8d16ffc1769499fb1c20503aad59305ced", size = 837039, upload-time = "2026-01-09T17:44:25.505Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6e/6f/34a9fba14d191a67f7e2ee3dbce3e9b86d2fa7310e2c7f2c713583481bd2/langchain_core-1.2.7-py3-none-any.whl", hash = "sha256:452f4fef7a3d883357b22600788d37e3d8854ef29da345b7ac7099f33c31828b", size = 490232, upload-time = "2026-01-09T17:44:24.236Z" },
+]
+
+[[package]]
+name = "langgraph-checkpoint"
+version = "4.0.0"
+source = { editable = "../checkpoint" }
+dependencies = [
+ { name = "langchain-core" },
+ { name = "ormsgpack" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "langchain-core", specifier = ">=0.2.38" },
+ { name = "ormsgpack", specifier = ">=1.12.0" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "codespell" },
+ { name = "dataclasses-json" },
+ { name = "mypy" },
+ { name = "numpy" },
+ { name = "pandas" },
+ { name = "pandas-stubs", specifier = ">=2.2.2.240807" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watcher" },
+ { name = "redis" },
+ { name = "ruff" },
+]
+lint = [
+ { name = "codespell" },
+ { name = "mypy" },
+ { name = "ruff" },
+]
+test = [
+ { name = "dataclasses-json" },
+ { name = "numpy" },
+ { name = "pandas" },
+ { name = "pandas-stubs", specifier = ">=2.2.2.240807" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watcher" },
+ { name = "redis" },
+]
+
+[[package]]
+name = "langgraph-checkpoint-sqlite"
+version = "3.0.3"
+source = { editable = "." }
+dependencies = [
+ { name = "aiosqlite" },
+ { name = "langgraph-checkpoint" },
+ { name = "sqlite-vec" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "codespell" },
+ { name = "langgraph-checkpoint" },
+ { name = "mypy" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-retry" },
+ { name = "pytest-watcher" },
+ { name = "ruff" },
+]
+lint = [
+ { name = "codespell" },
+ { name = "mypy" },
+ { name = "ruff" },
+]
+test = [
+ { name = "langgraph-checkpoint" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-retry" },
+ { name = "pytest-watcher" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "aiosqlite", specifier = ">=0.20" },
+ { name = "langgraph-checkpoint", editable = "../checkpoint" },
+ { name = "sqlite-vec", specifier = ">=0.1.6" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "codespell" },
+ { name = "langgraph-checkpoint", editable = "../checkpoint" },
+ { name = "mypy" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-retry", specifier = ">=1.7.0" },
+ { name = "pytest-watcher" },
+ { name = "ruff" },
+]
+lint = [
+ { name = "codespell" },
+ { name = "mypy" },
+ { name = "ruff" },
+]
+test = [
+ { name = "langgraph-checkpoint", editable = "../checkpoint" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-retry", specifier = ">=1.7.0" },
+ { name = "pytest-watcher" },
+]
+
+[[package]]
+name = "langsmith"
+version = "0.6.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "httpx" },
+ { name = "orjson", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "packaging" },
+ { name = "pydantic" },
+ { name = "requests" },
+ { name = "requests-toolbelt" },
+ { name = "uuid-utils" },
+ { name = "zstandard" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e7/85/9c7933052a997da1b85bc5c774f3865e9b1da1c8d71541ea133178b13229/langsmith-0.6.4.tar.gz", hash = "sha256:36f7223a01c218079fbb17da5e536ebbaf5c1468c028abe070aa3ae59bc99ec8", size = 919964, upload-time = "2026-01-15T20:02:28.873Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/66/0f/09a6637a7ba777eb307b7c80852d9ee26438e2bdafbad6fcc849ff9d9192/langsmith-0.6.4-py3-none-any.whl", hash = "sha256:ac4835860160be371042c7adbba3cb267bcf8d96a5ea976c33a8a4acad6c5486", size = 283503, upload-time = "2026-01-15T20:02:26.662Z" },
+]
+
+[[package]]
+name = "librt"
+version = "0.7.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" },
+ { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" },
+ { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" },
+ { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" },
+ { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" },
+ { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" },
+ { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" },
+ { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" },
+ { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" },
+ { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" },
+ { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" },
+ { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" },
+ { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" },
+ { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" },
+ { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" },
+ { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" },
+ { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" },
+ { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" },
+ { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" },
+ { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" },
+ { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" },
+ { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" },
+ { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" },
+ { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" },
+]
+
+[[package]]
+name = "mypy"
+version = "1.19.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "mypy-extensions" },
+ { name = "pathspec" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" },
+ { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" },
+ { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" },
+ { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
+ { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
+ { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" },
+ { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
+ { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
+ { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
+ { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
+ { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" },
+ { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" },
+ { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "orjson"
+version = "3.11.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/19/b22cf9dad4db20c8737041046054cbd4f38bb5a2d0e4bb60487832ce3d76/orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1", size = 245719, upload-time = "2025-12-06T15:53:43.877Z" },
+ { url = "https://files.pythonhosted.org/packages/03/2e/b136dd6bf30ef5143fbe76a4c142828b55ccc618be490201e9073ad954a1/orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870", size = 132467, upload-time = "2025-12-06T15:53:45.379Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/fc/ae99bfc1e1887d20a0268f0e2686eb5b13d0ea7bbe01de2b566febcd2130/orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09", size = 130702, upload-time = "2025-12-06T15:53:46.659Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/43/ef7912144097765997170aca59249725c3ab8ef6079f93f9d708dd058df5/orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd", size = 135907, upload-time = "2025-12-06T15:53:48.487Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/da/24d50e2d7f4092ddd4d784e37a3fa41f22ce8ed97abc9edd222901a96e74/orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac", size = 139935, upload-time = "2025-12-06T15:53:49.88Z" },
+ { url = "https://files.pythonhosted.org/packages/02/4a/b4cb6fcbfff5b95a3a019a8648255a0fac9b221fbf6b6e72be8df2361feb/orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e", size = 137541, upload-time = "2025-12-06T15:53:51.226Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/99/a11bd129f18c2377c27b2846a9d9be04acec981f770d711ba0aaea563984/orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f", size = 139031, upload-time = "2025-12-06T15:53:52.309Z" },
+ { url = "https://files.pythonhosted.org/packages/64/29/d7b77d7911574733a036bb3e8ad7053ceb2b7d6ea42208b9dbc55b23b9ed/orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18", size = 141622, upload-time = "2025-12-06T15:53:53.606Z" },
+ { url = "https://files.pythonhosted.org/packages/93/41/332db96c1de76b2feda4f453e91c27202cd092835936ce2b70828212f726/orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a", size = 413800, upload-time = "2025-12-06T15:53:54.866Z" },
+ { url = "https://files.pythonhosted.org/packages/76/e1/5a0d148dd1f89ad2f9651df67835b209ab7fcb1118658cf353425d7563e9/orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7", size = 151198, upload-time = "2025-12-06T15:53:56.383Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/96/8db67430d317a01ae5cf7971914f6775affdcfe99f5bff9ef3da32492ecc/orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401", size = 141984, upload-time = "2025-12-06T15:53:57.746Z" },
+ { url = "https://files.pythonhosted.org/packages/71/49/40d21e1aa1ac569e521069228bb29c9b5a350344ccf922a0227d93c2ed44/orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8", size = 135272, upload-time = "2025-12-06T15:53:59.769Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/7e/d0e31e78be0c100e08be64f48d2850b23bcb4d4c70d114f4e43b39f6895a/orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167", size = 133360, upload-time = "2025-12-06T15:54:01.25Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" },
+ { url = "https://files.pythonhosted.org/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c6/0a8caff96f4503f4f7dd44e40e90f4d14acf80d3b7a97cb88747bb712d3e/orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7", size = 130516, upload-time = "2025-12-06T15:54:06.274Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/63/43d4dc9bd9954bff7052f700fdb501067f6fb134a003ddcea2a0bb3854ed/orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd", size = 135695, upload-time = "2025-12-06T15:54:07.702Z" },
+ { url = "https://files.pythonhosted.org/packages/87/6f/27e2e76d110919cb7fcb72b26166ee676480a701bcf8fc53ac5d0edce32f/orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9", size = 139664, upload-time = "2025-12-06T15:54:08.828Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/f8/5966153a5f1be49b5fbb8ca619a529fde7bc71aa0a376f2bb83fed248bcd/orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef", size = 137289, upload-time = "2025-12-06T15:54:09.898Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/34/8acb12ff0299385c8bbcbb19fbe40030f23f15a6de57a9c587ebf71483fb/orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9", size = 138784, upload-time = "2025-12-06T15:54:11.022Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/27/910421ea6e34a527f73d8f4ee7bdffa48357ff79c7b8d6eb6f7b82dd1176/orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125", size = 141322, upload-time = "2025-12-06T15:54:12.427Z" },
+ { url = "https://files.pythonhosted.org/packages/87/a3/4b703edd1a05555d4bb1753d6ce44e1a05b7a6d7c164d5b332c795c63d70/orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814", size = 413612, upload-time = "2025-12-06T15:54:13.858Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/36/034177f11d7eeea16d3d2c42a1883b0373978e08bc9dad387f5074c786d8/orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5", size = 150993, upload-time = "2025-12-06T15:54:15.189Z" },
+ { url = "https://files.pythonhosted.org/packages/44/2f/ea8b24ee046a50a7d141c0227c4496b1180b215e728e3b640684f0ea448d/orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880", size = 141774, upload-time = "2025-12-06T15:54:16.451Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/12/cc440554bf8200eb23348a5744a575a342497b65261cd65ef3b28332510a/orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d", size = 135109, upload-time = "2025-12-06T15:54:17.73Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/83/e0c5aa06ba73a6760134b169f11fb970caa1525fa4461f94d76e692299d9/orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1", size = 133193, upload-time = "2025-12-06T15:54:19.426Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/35/5b77eaebc60d735e832c5b1a20b155667645d123f09d471db0a78280fb49/orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c", size = 126830, upload-time = "2025-12-06T15:54:20.836Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" },
+ { url = "https://files.pythonhosted.org/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" },
+ { url = "https://files.pythonhosted.org/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" },
+ { url = "https://files.pythonhosted.org/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" },
+ { url = "https://files.pythonhosted.org/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" },
+ { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" },
+ { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" },
+ { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" },
+ { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" },
+ { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" },
+ { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" },
+ { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" },
+ { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" },
+ { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" },
+ { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" },
+ { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" },
+ { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" },
+ { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" },
+ { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" },
+]
+
+[[package]]
+name = "ormsgpack"
+version = "1.12.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/93/fa/a91f70829ebccf6387c4946e0a1a109f6ba0d6a28d65f628bedfad94b890/ormsgpack-1.12.2-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c1429217f8f4d7fcb053523bbbac6bed5e981af0b85ba616e6df7cce53c19657", size = 378262, upload-time = "2026-01-18T20:55:22.284Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/62/3698a9a0c487252b5c6a91926e5654e79e665708ea61f67a8bdeceb022bf/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f13034dc6c84a6280c6c33db7ac420253852ea233fc3ee27c8875f8dd651163", size = 203034, upload-time = "2026-01-18T20:55:53.324Z" },
+ { url = "https://files.pythonhosted.org/packages/66/3a/f716f64edc4aec2744e817660b317e2f9bb8de372338a95a96198efa1ac1/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59f5da97000c12bc2d50e988bdc8576b21f6ab4e608489879d35b2c07a8ab51a", size = 210538, upload-time = "2026-01-18T20:55:20.097Z" },
+ { url = "https://files.pythonhosted.org/packages/72/30/a436be9ce27d693d4e19fa94900028067133779f09fc45776db3f689c822/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e4459c3f27066beadb2b81ea48a076a417aafffff7df1d3c11c519190ed44f2", size = 212401, upload-time = "2026-01-18T20:55:46.447Z" },
+ { url = "https://files.pythonhosted.org/packages/10/c5/cde98300fd33fee84ca71de4751b19aeeca675f0cf3c0ec4b043f40f3b76/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a1c460655d7288407ffa09065e322a7231997c0d62ce914bf3a96ad2dc6dedd", size = 387080, upload-time = "2026-01-18T20:56:00.884Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/31/30bf445ef827546747c10889dd254b3d84f92b591300efe4979d792f4c41/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:458e4568be13d311ef7d8877275e7ccbe06c0e01b39baaac874caaa0f46d826c", size = 482346, upload-time = "2026-01-18T20:55:39.831Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/f5/e1745ddf4fa246c921b5ca253636c4c700ff768d78032f79171289159f6e/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8cde5eaa6c6cbc8622db71e4a23de56828e3d876aeb6460ffbcb5b8aff91093b", size = 425178, upload-time = "2026-01-18T20:55:27.106Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/a2/e6532ed7716aed03dede8df2d0d0d4150710c2122647d94b474147ccd891/ormsgpack-1.12.2-cp310-cp310-win_amd64.whl", hash = "sha256:dc7a33be14c347893edbb1ceda89afbf14c467d593a5ee92c11de4f1666b4d4f", size = 117183, upload-time = "2026-01-18T20:55:55.52Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9", size = 378266, upload-time = "2026-01-18T20:55:59.876Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a", size = 203035, upload-time = "2026-01-18T20:55:30.59Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5", size = 210539, upload-time = "2026-01-18T20:55:48.569Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/33/543627f323ff3c73091f51d6a20db28a1a33531af30873ea90c5ac95a9b5/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181", size = 212401, upload-time = "2026-01-18T20:56:10.101Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/5d/f70e2c3da414f46186659d24745483757bcc9adccb481a6eb93e2b729301/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b", size = 387082, upload-time = "2026-01-18T20:56:12.047Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/d6/06e8dc920c7903e051f30934d874d4afccc9bb1c09dcaf0bc03a7de4b343/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92", size = 482346, upload-time = "2026-01-18T20:56:05.152Z" },
+ { url = "https://files.pythonhosted.org/packages/66/c4/f337ac0905eed9c393ef990c54565cd33644918e0a8031fe48c098c71dbf/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a", size = 425181, upload-time = "2026-01-18T20:55:37.83Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/6d5758fabef3babdf4bbbc453738cc7de9cd3334e4c38dd5737e27b85653/ormsgpack-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c", size = 117182, upload-time = "2026-01-18T20:55:31.472Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/57/17a15549233c37e7fd054c48fe9207492e06b026dbd872b826a0b5f833b6/ormsgpack-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd", size = 111464, upload-time = "2026-01-18T20:55:38.811Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" },
+ { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" },
+ { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" },
+ { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" },
+ { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" },
+ { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" },
+ { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" },
+ { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" },
+ { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" },
+ { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "1.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" },
+ { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" },
+ { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" },
+ { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+ { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
+ { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" },
+ { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" },
+ { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
+ { name = "pytest" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
+]
+
+[[package]]
+name = "pytest-mock"
+version = "3.15.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
+]
+
+[[package]]
+name = "pytest-retry"
+version = "1.7.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c5/5b/607b017994cca28de3a1ad22a3eee8418e5d428dcd8ec25b26b18e995a73/pytest_retry-1.7.0.tar.gz", hash = "sha256:f8d52339f01e949df47c11ba9ee8d5b362f5824dff580d3870ec9ae0057df80f", size = 19977, upload-time = "2025-01-19T01:56:13.115Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7c/ff/3266c8a73b9b93c4b14160a7e2b31d1e1088e28ed29f4c2d93ae34093bfd/pytest_retry-1.7.0-py3-none-any.whl", hash = "sha256:a2dac85b79a4e2375943f1429479c65beb6c69553e7dae6b8332be47a60954f4", size = 13775, upload-time = "2025-01-19T01:56:11.199Z" },
+]
+
+[[package]]
+name = "pytest-watcher"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "watchdog" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e6/d2/80606077b7fa8784417687f494ff801d7ab817d9a17fc94305811d5919bb/pytest_watcher-0.6.3.tar.gz", hash = "sha256:842dc904264df0ad2d5264153a66bb452fccfa46598cd6e0a5ef1d19afed9b13", size = 601878, upload-time = "2026-01-10T23:28:18.805Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/3f/172d73600ad2771774cda108efb813fc724fc345e5240a81a1085f1ade5d/pytest_watcher-0.6.3-py3-none-any.whl", hash = "sha256:83e7748c933087e8276edb6078663e6afa9926434b4fd8b85cf6b32b1d5bec89", size = 12431, upload-time = "2026-01-10T23:28:17.64Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
+ { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
+ { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+]
+
+[[package]]
+name = "requests-toolbelt"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.14.13"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" },
+ { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" },
+ { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" },
+ { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" },
+ { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" },
+]
+
+[[package]]
+name = "sqlite-vec"
+version = "0.1.6"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/ed/aabc328f29ee6814033d008ec43e44f2c595447d9cccd5f2aabe60df2933/sqlite_vec-0.1.6-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:77491bcaa6d496f2acb5cc0d0ff0b8964434f141523c121e313f9a7d8088dee3", size = 164075, upload-time = "2024-11-20T16:40:29.847Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/57/05604e509a129b22e303758bfa062c19afb020557d5e19b008c64016704e/sqlite_vec-0.1.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fdca35f7ee3243668a055255d4dee4dea7eed5a06da8cad409f89facf4595361", size = 165242, upload-time = "2024-11-20T16:40:31.206Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/48/dbb2cc4e5bad88c89c7bb296e2d0a8df58aab9edc75853728c361eefc24f/sqlite_vec-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b0519d9cd96164cd2e08e8eed225197f9cd2f0be82cb04567692a0a4be02da3", size = 103704, upload-time = "2024-11-20T16:40:33.729Z" },
+ { url = "https://files.pythonhosted.org/packages/80/76/97f33b1a2446f6ae55e59b33869bed4eafaf59b7f4c662c8d9491b6a714a/sqlite_vec-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:823b0493add80d7fe82ab0fe25df7c0703f4752941aee1c7b2b02cec9656cb24", size = 151556, upload-time = "2024-11-20T16:40:35.387Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/98/e8bc58b178266eae2fcf4c9c7a8303a8d41164d781b32d71097924a6bebe/sqlite_vec-0.1.6-py3-none-win_amd64.whl", hash = "sha256:c65bcfd90fa2f41f9000052bcb8bb75d38240b2dae49225389eca6c3136d3f0c", size = 281540, upload-time = "2024-11-20T16:40:37.296Z" },
+]
+
+[[package]]
+name = "tenacity"
+version = "9.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
+ { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
+ { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
+ { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
+ { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
+ { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
+ { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
+ { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
+ { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
+ { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
+ { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
+ { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
+ { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
+]
+
+[[package]]
+name = "uuid-utils"
+version = "0.13.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/8a/17b11768dcb473d3a255c02ffdd94fbd1b345c906efea0a39124dcbaed52/uuid_utils-0.13.0.tar.gz", hash = "sha256:4c17df6427a9e23a4cd7fb9ee1efb53b8abb078660b9bdb2524ca8595022dfe1", size = 21921, upload-time = "2026-01-08T15:48:10.841Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/b8/d40848ca22781f206c60a1885fc737d2640392bd6b5792d455525accd89c/uuid_utils-0.13.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:83628283e977fb212e756bc055df8fdd2f9f589a2e539ba1abe755b8ce8df7a4", size = 602130, upload-time = "2026-01-08T15:47:34.877Z" },
+ { url = "https://files.pythonhosted.org/packages/40/b9/00a944b8096632ea12638181f8e294abcde3e3b8b5e29b777f809896f6ae/uuid_utils-0.13.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c47638ed6334ab19d80f73664f153b04bbb04ab8ce4298d10da6a292d4d21c47", size = 304213, upload-time = "2026-01-08T15:47:36.807Z" },
+ { url = "https://files.pythonhosted.org/packages/da/d7/07b36c33aef683b81c9afff3ec178d5eb39d325447a68c3c68a62e4abb32/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:b276b538c57733ed406948584912da422a604313c71479654848b84b9e19c9b0", size = 340624, upload-time = "2026-01-08T15:47:38.821Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/55/fcff2fff02a27866cb1a6614c9df2b3ace721f0a0aab2b7b8f5a7d4e4221/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_armv7l.whl", hash = "sha256:bdaf2b77e34b199cf04cde28399495fd1ed951de214a4ece1f3919b2f945bb06", size = 346705, upload-time = "2026-01-08T15:47:40.397Z" },
+ { url = "https://files.pythonhosted.org/packages/41/48/67438506c2bb8bee1b4b00d7c0b3ff866401b4790849bf591d654d4ea0bc/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_i686.whl", hash = "sha256:eb2f0baf81e82f9769a7684022dca8f3bf801ca1574a3e94df1876e9d6f9271e", size = 366023, upload-time = "2026-01-08T15:47:42.662Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/d7/2d91ce17f62fd764d593430de296b70843cc25229c772453f7261de9e6a8/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_ppc64le.whl", hash = "sha256:6be6c4d11275f5cc402a4fdba6c2b1ce45fd3d99bb78716cd1cc2cbf6802b2ce", size = 471149, upload-time = "2026-01-08T15:47:44.963Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/9a/aa0756186073ba84daf5704c150d41ede10eb3185d510e02532e2071550e/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:77621cf6ceca7f42173a642a01c01c216f9eaec3b7b65d093d2d6a433ca0a83d", size = 342130, upload-time = "2026-01-08T15:47:46.331Z" },
+ { url = "https://files.pythonhosted.org/packages/74/b4/3191789f4dc3bed59d79cec90559821756297a25d7dc34d1bf7781577a75/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a5a9eb06c2bb86dd876cd7b2fe927fc8543d14c90d971581db6ffda4a02526f", size = 524128, upload-time = "2026-01-08T15:47:47.628Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/30/29839210a8fff9fc219bfa7c8d8cd115324e92618cba0cda090d54d3d321/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:775347c6110fb71360df17aac74132d8d47c1dbe71233ac98197fc872a791fd2", size = 615872, upload-time = "2026-01-08T15:47:50.61Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ed/15000c96a8bd8f5fd8efd622109bf52549ea0b366f8ce71c45580fa55878/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf95f6370ad1a0910ee7b5ad5228fd19c4ae32fe3627389006adaf519408c41e", size = 581023, upload-time = "2026-01-08T15:47:52.776Z" },
+ { url = "https://files.pythonhosted.org/packages/67/c8/3f809fa2dc2ca4bd331c792a3c7d3e45ae2b709d85847a12b8b27d1d5f19/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a88e23e0b2f4203fefe2ccbca5736ee06fcad10e61b5e7e39c8d7904bc13300", size = 546715, upload-time = "2026-01-08T15:47:54.415Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/80/4f7c7efd734d1494397c781bd3d421688e9c187ae836e3174625b1ddf8b0/uuid_utils-0.13.0-cp39-abi3-win32.whl", hash = "sha256:3e4f2cc54e6a99c0551158100ead528479ad2596847478cbad624977064ffce3", size = 177650, upload-time = "2026-01-08T15:47:55.679Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/94/d05ab68622e66ad787a241dfe5ccc649b3af09f30eae977b9ee8f7046aaa/uuid_utils-0.13.0-cp39-abi3-win_amd64.whl", hash = "sha256:046cb2756e1597b3de22d24851b769913e192135830486a0a70bf41327f0360c", size = 183211, upload-time = "2026-01-08T15:47:57.604Z" },
+ { url = "https://files.pythonhosted.org/packages/69/37/674b3ce25cd715b831ea8ebbd828b74c40159f04c95d1bb963b2c876fe79/uuid_utils-0.13.0-cp39-abi3-win_arm64.whl", hash = "sha256:5447a680df6ef8a5a353976aaf4c97cc3a3a22b1ee13671c44227b921e3ae2a9", size = 183518, upload-time = "2026-01-08T15:47:59.148Z" },
+ { url = "https://files.pythonhosted.org/packages/99/fa/1d92de9538463859228e68db679b766fd300770c9a2db849dcba0c0c5a57/uuid_utils-0.13.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e5182e2d95f38e65f2e5bce90648ef56987443da13e145afcd747e584f9bc69c", size = 587641, upload-time = "2026-01-08T15:48:00.433Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/07/6bd9e6f5367e38c2ee7178ad882d2bd1b0d17c5393974b09ab027a215eba/uuid_utils-0.13.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e3909a8a1fbd79d7c8bdc874eeb83e23ccb7a7cb0aa821a49596cc96c0cce84b", size = 298273, upload-time = "2026-01-08T15:48:02.063Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/14/7061b868a8a6799c8df83768a23f313d4e22075069f01ee3c28fa82aa2c6/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:5dc4c9f749bd2511b8dcbf0891e658d7d86880022963db050722ad7b502b5e22", size = 333618, upload-time = "2026-01-08T15:48:03.503Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/f1/f48c3c9c343c9071ade5f355403e344d817412d9cf379a2d04b181282e74/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_armv7l.whl", hash = "sha256:516adf07f5b2cdb88d50f489c702b5f1a75ae8b2639bfd254f4192d5f7ee261f", size = 339104, upload-time = "2026-01-08T15:48:05.02Z" },
+ { url = "https://files.pythonhosted.org/packages/47/22/8e3142b4baffee77ce533fe956446d3699ec42f1d5252911208cbef4501e/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_i686.whl", hash = "sha256:aeee3bd89e8de6184a3ab778ce19f5ce9ad32849d1be549516e0ddb257562d8d", size = 359503, upload-time = "2026-01-08T15:48:06.347Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/1a/756f1f9e31b15019c87cd2becb1c596351c50967cd143443da38df8818d1/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_ppc64le.whl", hash = "sha256:97985256c2e59b7caa51f5c8515f64d777328562a9c900ec65e9d627baf72737", size = 467480, upload-time = "2026-01-08T15:48:07.681Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/20/a6929e98d9a461ca49e96194a82a1cc3fd5420f3a2f53cbb34fca438549e/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:b7ccaa20e24c5f60f41a69ef571ed820737f9b0ade4cbeef56aaa8f80f5aa475", size = 333610, upload-time = "2026-01-08T15:48:09.375Z" },
+]
+
+[[package]]
+name = "watchdog"
+version = "6.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" },
+ { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" },
+ { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" },
+ { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" },
+ { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
+ { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
+ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
+ { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
+ { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
+ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
+ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
+]
+
+[[package]]
+name = "zstandard"
+version = "0.25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" },
+ { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" },
+ { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" },
+ { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" },
+ { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" },
+ { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" },
+ { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" },
+ { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" },
+ { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" },
+ { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" },
+ { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" },
+ { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" },
+ { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" },
+ { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" },
+ { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" },
+ { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" },
+ { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" },
+ { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" },
+ { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" },
+ { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" },
+ { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" },
+ { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" },
+ { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" },
+ { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" },
+ { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" },
+ { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" },
+]
diff --git a/langgraph/source/libs/checkpoint/LICENSE b/langgraph/source/libs/checkpoint/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..fc0602feecdd6748623c852ab534e1ca612673c7
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 LangChain, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/langgraph/source/libs/checkpoint/Makefile b/langgraph/source/libs/checkpoint/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..376c05d74cadc15760b1dcedb4213dedf67ad3a6
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/Makefile
@@ -0,0 +1,37 @@
+.PHONY: test test_watch lint format
+
+######################
+# TESTING AND COVERAGE
+######################
+
+TEST ?= .
+
+test:
+ uv run pytest $(TEST)
+
+test_watch:
+ uv run ptw $(TEST)
+
+######################
+# LINTING AND FORMATTING
+######################
+
+# Define a variable for Python and notebook files.
+PYTHON_FILES=.
+MYPY_CACHE=.mypy_cache
+lint format: PYTHON_FILES=.
+lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --relative --diff-filter=d main . | grep -E '\.py$$|\.ipynb$$')
+lint_package: PYTHON_FILES=langgraph
+lint_tests: PYTHON_FILES=tests
+lint_tests: MYPY_CACHE=.mypy_cache_test
+
+lint lint_diff lint_package lint_tests:
+ uv run ruff check .
+ [ "$(PYTHON_FILES)" = "" ] || uv run ruff format $(PYTHON_FILES) --diff
+ [ "$(PYTHON_FILES)" = "" ] || uv run ruff check --select I $(PYTHON_FILES)
+ [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE)
+ [ "$(PYTHON_FILES)" = "" ] || uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
+
+format format_diff:
+ uv run ruff format $(PYTHON_FILES)
+ uv run ruff check --select I --fix $(PYTHON_FILES)
diff --git a/langgraph/source/libs/checkpoint/README.md b/langgraph/source/libs/checkpoint/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4152c3824810825311e6525037a7b13b02e62519
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/README.md
@@ -0,0 +1,88 @@
+# LangGraph Checkpoint
+
+This library defines the base interface for LangGraph checkpointers. Checkpointers provide a persistence layer for LangGraph. They allow you to interact with and manage the graph's state. When you use a graph with a checkpointer, the checkpointer saves a _checkpoint_ of the graph state at every superstep, enabling several powerful capabilities like human-in-the-loop, "memory" between interactions and more.
+
+## Key concepts
+
+### Checkpoint
+
+Checkpoint is a snapshot of the graph state at a given point in time. Checkpoint tuple refers to an object containing checkpoint and the associated config, metadata and pending writes.
+
+### Thread
+
+Threads enable the checkpointing of multiple different runs, making them essential for multi-tenant chat applications and other scenarios where maintaining separate states is necessary. A thread is a unique ID assigned to a series of checkpoints saved by a checkpointer. When using a checkpointer, you must specify a `thread_id` and optionally `checkpoint_id` when running the graph.
+
+- `thread_id` is simply the ID of a thread. This is always required.
+- `checkpoint_id` can optionally be passed. This identifier refers to a specific checkpoint within a thread. This can be used to kick off a run of a graph from some point halfway through a thread.
+
+You must pass these when invoking the graph as part of the configurable part of the config, e.g.
+
+```python
+{"configurable": {"thread_id": "1"}} # valid config
+{"configurable": {"thread_id": "1", "checkpoint_id": "0c62ca34-ac19-445d-bbb0-5b4984975b2a"}} # also valid config
+```
+
+### Serde
+
+`langgraph_checkpoint` also defines protocol for serialization/deserialization (serde) and provides an default implementation (`langgraph.checkpoint.serde.jsonplus.JsonPlusSerializer`) that handles a wide variety of types, including LangChain and LangGraph primitives, datetimes, enums and more.
+
+### Pending writes
+
+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.
+
+## Interface
+
+Each checkpointer should conform to `langgraph.checkpoint.base.BaseCheckpointSaver` interface and must implement the following methods:
+
+- `.put` - Store a checkpoint with its configuration and metadata.
+- `.put_writes` - Store intermediate writes linked to a checkpoint (i.e. pending writes).
+- `.get_tuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `checkpoint_id`).
+- `.list` - List checkpoints that match a given configuration and filter criteria.
+- `.delete_thread()` - Delete all checkpoints and writes associated with a thread.
+- `.get_next_version()` - Generate the next version ID for a channel.
+
+If the checkpointer will be used with asynchronous graph execution (i.e. executing the graph via `.ainvoke`, `.astream`, `.abatch`), checkpointer must implement asynchronous versions of the above methods (`.aput`, `.aput_writes`, `.aget_tuple`, `.alist`). Similarly, the checkpointer must implement `.adelete_thread()` if asynchronous thread cleanup is desired. The base class provides a default implementation of `.get_next_version()` that generates an integer sequence starting from 1, but this method should be overridden for custom versioning schemes.
+
+## Usage
+
+```python
+from langgraph.checkpoint.memory import InMemorySaver
+
+write_config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
+read_config = {"configurable": {"thread_id": "1"}}
+
+checkpointer = InMemorySaver()
+checkpoint = {
+ "v": 4,
+ "ts": "2024-07-31T20:14:19.804150+00:00",
+ "id": "1ef4f797-8335-6428-8001-8a1503f9b875",
+ "channel_values": {
+ "my_key": "meow",
+ "node": "node"
+ },
+ "channel_versions": {
+ "__start__": 2,
+ "my_key": 3,
+ "start:node": 3,
+ "node": 3
+ },
+ "versions_seen": {
+ "__input__": {},
+ "__start__": {
+ "__start__": 1
+ },
+ "node": {
+ "start:node": 2
+ }
+ },
+}
+
+# store checkpoint
+checkpointer.put(write_config, checkpoint, {}, {})
+
+# load checkpoint
+checkpointer.get(read_config)
+
+# list checkpoints
+list(checkpointer.list(read_config))
+```
diff --git a/langgraph/source/libs/checkpoint/__init__.py b/langgraph/source/libs/checkpoint/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/checkpoint/langgraph/__init__.py b/langgraph/source/libs/checkpoint/langgraph/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/checkpoint/langgraph/cache/__init__.py b/langgraph/source/libs/checkpoint/langgraph/cache/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/cache/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/checkpoint/langgraph/cache/base/__init__.py b/langgraph/source/libs/checkpoint/langgraph/cache/base/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..092324e721d4717bdc35bc843ee0c317aabaf19c
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/cache/base/__init__.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from collections.abc import Mapping, Sequence
+from typing import Generic, TypeVar
+
+from langgraph.checkpoint.serde.base import SerializerProtocol
+from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
+
+ValueT = TypeVar("ValueT")
+Namespace = tuple[str, ...]
+FullKey = tuple[Namespace, str]
+
+
+class BaseCache(ABC, Generic[ValueT]):
+ """Base class for a cache."""
+
+ serde: SerializerProtocol = JsonPlusSerializer(pickle_fallback=False)
+
+ def __init__(self, *, serde: SerializerProtocol | None = None) -> None:
+ """Initialize the cache with a serializer."""
+ self.serde = serde or self.serde
+
+ @abstractmethod
+ def get(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
+ """Get the cached values for the given keys."""
+
+ @abstractmethod
+ async def aget(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
+ """Asynchronously get the cached values for the given keys."""
+
+ @abstractmethod
+ def set(self, pairs: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
+ """Set the cached values for the given keys and TTLs."""
+
+ @abstractmethod
+ async def aset(self, pairs: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
+ """Asynchronously set the cached values for the given keys and TTLs."""
+
+ @abstractmethod
+ def clear(self, namespaces: Sequence[Namespace] | None = None) -> None:
+ """Delete the cached values for the given namespaces.
+ If no namespaces are provided, clear all cached values."""
+
+ @abstractmethod
+ async def aclear(self, namespaces: Sequence[Namespace] | None = None) -> None:
+ """Asynchronously delete the cached values for the given namespaces.
+ If no namespaces are provided, clear all cached values."""
diff --git a/langgraph/source/libs/checkpoint/langgraph/cache/base/py.typed b/langgraph/source/libs/checkpoint/langgraph/cache/base/py.typed
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/checkpoint/langgraph/cache/memory/__init__.py b/langgraph/source/libs/checkpoint/langgraph/cache/memory/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..7b10db0511c62f2e4c98a19a8bc796599db71124
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/cache/memory/__init__.py
@@ -0,0 +1,73 @@
+from __future__ import annotations
+
+import datetime
+import threading
+from collections.abc import Mapping, Sequence
+
+from langgraph.cache.base import BaseCache, FullKey, Namespace, ValueT
+from langgraph.checkpoint.serde.base import SerializerProtocol
+
+
+class InMemoryCache(BaseCache[ValueT]):
+ def __init__(self, *, serde: SerializerProtocol | None = None):
+ super().__init__(serde=serde)
+ self._cache: dict[Namespace, dict[str, tuple[str, bytes, float | None]]] = {}
+ self._lock = threading.RLock()
+
+ def get(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
+ """Get the cached values for the given keys."""
+ with self._lock:
+ if not keys:
+ return {}
+ now = datetime.datetime.now(datetime.timezone.utc).timestamp()
+ values: dict[FullKey, ValueT] = {}
+ for ns_tuple, key in keys:
+ ns = Namespace(ns_tuple)
+ if ns in self._cache and key in self._cache[ns]:
+ enc, val, expiry = self._cache[ns][key]
+ if expiry is None or now < expiry:
+ values[(ns, key)] = self.serde.loads_typed((enc, val))
+ else:
+ del self._cache[ns][key]
+ return values
+
+ async def aget(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
+ """Asynchronously get the cached values for the given keys."""
+ return self.get(keys)
+
+ def set(self, keys: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
+ """Set the cached values for the given keys."""
+ with self._lock:
+ now = datetime.datetime.now(datetime.timezone.utc)
+ for (ns, key), (value, ttl) in keys.items():
+ if ttl is not None:
+ delta = datetime.timedelta(seconds=ttl)
+ expiry: float | None = (now + delta).timestamp()
+ else:
+ expiry = None
+ if ns not in self._cache:
+ self._cache[ns] = {}
+ self._cache[ns][key] = (
+ *self.serde.dumps_typed(value),
+ expiry,
+ )
+
+ async def aset(self, keys: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
+ """Asynchronously set the cached values for the given keys."""
+ self.set(keys)
+
+ def clear(self, namespaces: Sequence[Namespace] | None = None) -> None:
+ """Delete the cached values for the given namespaces.
+ If no namespaces are provided, clear all cached values."""
+ with self._lock:
+ if namespaces is None:
+ self._cache.clear()
+ else:
+ for ns in namespaces:
+ if ns in self._cache:
+ del self._cache[ns]
+
+ async def aclear(self, namespaces: Sequence[Namespace] | None = None) -> None:
+ """Asynchronously delete the cached values for the given namespaces.
+ If no namespaces are provided, clear all cached values."""
+ self.clear(namespaces)
diff --git a/langgraph/source/libs/checkpoint/langgraph/cache/redis/__init__.py b/langgraph/source/libs/checkpoint/langgraph/cache/redis/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..ea4f71480d02a01ec242335d32ac532f61ccfb7d
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/cache/redis/__init__.py
@@ -0,0 +1,144 @@
+from __future__ import annotations
+
+from collections.abc import Mapping, Sequence
+from typing import Any
+
+from langgraph.cache.base import BaseCache, FullKey, Namespace, ValueT
+from langgraph.checkpoint.serde.base import SerializerProtocol
+
+
+class RedisCache(BaseCache[ValueT]):
+ """Redis-based cache implementation with TTL support."""
+
+ def __init__(
+ self,
+ redis: Any,
+ *,
+ serde: SerializerProtocol | None = None,
+ prefix: str = "langgraph:cache:",
+ ) -> None:
+ """Initialize the cache with a Redis client.
+
+ Args:
+ redis: Redis client instance (sync or async)
+ serde: Serializer to use for values
+ prefix: Key prefix for all cached values
+ """
+ super().__init__(serde=serde)
+ self.redis = redis
+ self.prefix = prefix
+
+ def _make_key(self, ns: Namespace, key: str) -> str:
+ """Create a Redis key from namespace and key."""
+ ns_str = ":".join(ns) if ns else ""
+ return f"{self.prefix}{ns_str}:{key}" if ns_str else f"{self.prefix}{key}"
+
+ def _parse_key(self, redis_key: str) -> tuple[Namespace, str]:
+ """Parse a Redis key back to namespace and key."""
+ if not redis_key.startswith(self.prefix):
+ raise ValueError(
+ f"Key {redis_key} does not start with prefix {self.prefix}"
+ )
+
+ remaining = redis_key[len(self.prefix) :]
+ if ":" in remaining:
+ parts = remaining.split(":")
+ key = parts[-1]
+ ns_parts = parts[:-1]
+ return (tuple(ns_parts), key)
+ else:
+ return (tuple(), remaining)
+
+ def get(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
+ """Get the cached values for the given keys."""
+ if not keys:
+ return {}
+
+ # Build Redis keys
+ redis_keys = [self._make_key(ns, key) for ns, key in keys]
+
+ # Get values from Redis using MGET
+ try:
+ raw_values = self.redis.mget(redis_keys)
+ except Exception:
+ # If Redis is unavailable, return empty dict
+ return {}
+
+ values: dict[FullKey, ValueT] = {}
+ for i, raw_value in enumerate(raw_values):
+ if raw_value is not None:
+ try:
+ # Deserialize the value
+ encoding, data = raw_value.split(b":", 1)
+ values[keys[i]] = self.serde.loads_typed((encoding.decode(), data))
+ except Exception:
+ # Skip corrupted entries
+ continue
+
+ return values
+
+ async def aget(self, keys: Sequence[FullKey]) -> dict[FullKey, ValueT]:
+ """Asynchronously get the cached values for the given keys."""
+ return self.get(keys)
+
+ def set(self, mapping: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
+ """Set the cached values for the given keys and TTLs."""
+ if not mapping:
+ return
+
+ # Use pipeline for efficient batch operations
+ pipe = self.redis.pipeline()
+
+ for (ns, key), (value, ttl) in mapping.items():
+ redis_key = self._make_key(ns, key)
+ encoding, data = self.serde.dumps_typed(value)
+
+ # Store as "encoding:data" format
+ serialized_value = f"{encoding}:".encode() + data
+
+ if ttl is not None:
+ pipe.setex(redis_key, ttl, serialized_value)
+ else:
+ pipe.set(redis_key, serialized_value)
+
+ try:
+ pipe.execute()
+ except Exception:
+ # Silently fail if Redis is unavailable
+ pass
+
+ async def aset(self, mapping: Mapping[FullKey, tuple[ValueT, int | None]]) -> None:
+ """Asynchronously set the cached values for the given keys and TTLs."""
+ self.set(mapping)
+
+ def clear(self, namespaces: Sequence[Namespace] | None = None) -> None:
+ """Delete the cached values for the given namespaces.
+ If no namespaces are provided, clear all cached values."""
+ try:
+ if namespaces is None:
+ # Clear all keys with our prefix
+ pattern = f"{self.prefix}*"
+ keys = self.redis.keys(pattern)
+ if keys:
+ self.redis.delete(*keys)
+ else:
+ # Clear specific namespaces
+ keys_to_delete = []
+ for ns in namespaces:
+ ns_str = ":".join(ns) if ns else ""
+ pattern = (
+ f"{self.prefix}{ns_str}:*" if ns_str else f"{self.prefix}*"
+ )
+ keys = self.redis.keys(pattern)
+ keys_to_delete.extend(keys)
+
+ if keys_to_delete:
+ self.redis.delete(*keys_to_delete)
+ except Exception:
+ # Silently fail if Redis is unavailable
+ pass
+
+ async def aclear(self, namespaces: Sequence[Namespace] | None = None) -> None:
+ """Asynchronously delete the cached values for the given namespaces.
+ If no namespaces are provided, clear all cached values."""
+ self.clear(namespaces)
diff --git a/langgraph/source/libs/checkpoint/langgraph/checkpoint/__init__.py b/langgraph/source/libs/checkpoint/langgraph/checkpoint/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/checkpoint/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/checkpoint/langgraph/checkpoint/base/__init__.py b/langgraph/source/libs/checkpoint/langgraph/checkpoint/base/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..f0e05c01453a915a1eec3e857fa7418ce804c844
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/checkpoint/base/__init__.py
@@ -0,0 +1,513 @@
+from __future__ import annotations
+
+from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
+from typing import ( # noqa: UP035
+ Any,
+ Generic,
+ Literal,
+ NamedTuple,
+ TypedDict,
+ TypeVar,
+)
+
+from langchain_core.runnables import RunnableConfig
+
+from langgraph.checkpoint.base.id import uuid6
+from langgraph.checkpoint.serde.base import SerializerProtocol, maybe_add_typed_methods
+from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
+from langgraph.checkpoint.serde.types import (
+ ERROR,
+ INTERRUPT,
+ RESUME,
+ SCHEDULED,
+ ChannelProtocol,
+)
+
+V = TypeVar("V", int, float, str)
+PendingWrite = tuple[str, str, Any]
+
+
+# Marked as total=False to allow for future expansion.
+class CheckpointMetadata(TypedDict, total=False):
+ """Metadata associated with a checkpoint."""
+
+ source: Literal["input", "loop", "update", "fork"]
+ """The source of the checkpoint.
+
+ - `"input"`: The checkpoint was created from an input to invoke/stream/batch.
+ - `"loop"`: The checkpoint was created from inside the pregel loop.
+ - `"update"`: The checkpoint was created from a manual state update.
+ - `"fork"`: The checkpoint was created as a copy of another checkpoint.
+ """
+ step: int
+ """The step number of the checkpoint.
+
+ `-1` for the first `"input"` checkpoint.
+ `0` for the first `"loop"` checkpoint.
+ `...` for the `nth` checkpoint afterwards.
+ """
+ parents: dict[str, str]
+ """The IDs of the parent checkpoints.
+
+ Mapping from checkpoint namespace to checkpoint ID.
+ """
+
+
+ChannelVersions = dict[str, str | int | float]
+
+
+class Checkpoint(TypedDict):
+ """State snapshot at a given point in time."""
+
+ v: int
+ """The version of the checkpoint format. Currently `1`."""
+ id: str
+ """The ID of the checkpoint.
+
+ This is both unique and monotonically increasing, so can be used for sorting
+ checkpoints from first to last."""
+ ts: str
+ """The timestamp of the checkpoint in ISO 8601 format."""
+ channel_values: dict[str, Any]
+ """The values of the channels at the time of the checkpoint.
+
+ Mapping from channel name to deserialized channel snapshot value.
+ """
+ channel_versions: ChannelVersions
+ """The versions of the channels at the time of the checkpoint.
+
+ The keys are channel names and the values are monotonically increasing
+ version strings for each channel.
+ """
+ versions_seen: dict[str, ChannelVersions]
+ """Map from node ID to map from channel name to version seen.
+
+ This keeps track of the versions of the channels that each node has seen.
+ Used to determine which nodes to execute next.
+ """
+ updated_channels: list[str] | None
+ """The channels that were updated in this checkpoint.
+ """
+
+
+def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint:
+ return Checkpoint(
+ v=checkpoint["v"],
+ ts=checkpoint["ts"],
+ id=checkpoint["id"],
+ channel_values=checkpoint["channel_values"].copy(),
+ channel_versions=checkpoint["channel_versions"].copy(),
+ versions_seen={k: v.copy() for k, v in checkpoint["versions_seen"].items()},
+ pending_sends=checkpoint.get("pending_sends", []).copy(),
+ updated_channels=checkpoint.get("updated_channels", None),
+ )
+
+
+class CheckpointTuple(NamedTuple):
+ """A tuple containing a checkpoint and its associated data."""
+
+ config: RunnableConfig
+ checkpoint: Checkpoint
+ metadata: CheckpointMetadata
+ parent_config: RunnableConfig | None = None
+ pending_writes: list[PendingWrite] | None = None
+
+
+class BaseCheckpointSaver(Generic[V]):
+ """Base class for creating a graph checkpointer.
+
+ Checkpointers allow LangGraph agents to persist their state
+ within and across multiple interactions.
+
+ When a checkpointer is configured, you should pass a `thread_id` in the config when
+ invoking the graph:
+
+ ```python
+ config = {"configurable": {"thread_id": "my-thread"}}
+ graph.invoke(inputs, config)
+ ```
+
+ The `thread_id` is the primary key used to store and retrieve checkpoints. Without
+ it, the checkpointer cannot save state, resume from interrupts, or enable
+ time-travel debugging.
+
+ How you choose ``thread_id`` depends on your use case:
+
+ - **Single-shot workflows**: Use a unique ID (e.g., uuid4) for each run when
+ executions are independent.
+ - **Conversational memory**: Reuse the same `thread_id` across invocations
+ to accumulate state (e.g., chat history) within a conversation.
+
+ Attributes:
+ serde (SerializerProtocol): Serializer for encoding/decoding checkpoints.
+
+ Note:
+ When creating a custom checkpoint saver, consider implementing async
+ versions to avoid blocking the main thread.
+ """
+
+ serde: SerializerProtocol = JsonPlusSerializer()
+
+ def __init__(
+ self,
+ *,
+ serde: SerializerProtocol | None = None,
+ ) -> None:
+ self.serde = maybe_add_typed_methods(serde or self.serde)
+
+ @property
+ def config_specs(self) -> list:
+ """Define the configuration options for the checkpoint saver.
+
+ Returns:
+ list: List of configuration field specs.
+ """
+ return []
+
+ def get(self, config: RunnableConfig) -> Checkpoint | None:
+ """Fetch a checkpoint using the given configuration.
+
+ Args:
+ config: Configuration specifying which checkpoint to retrieve.
+
+ Returns:
+ The requested checkpoint, or `None` if not found.
+ """
+ if value := self.get_tuple(config):
+ return value.checkpoint
+
+ def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
+ """Fetch a checkpoint tuple using the given configuration.
+
+ Args:
+ config: Configuration specifying which checkpoint to retrieve.
+
+ Returns:
+ The requested checkpoint tuple, or `None` if not found.
+
+ Raises:
+ NotImplementedError: Implement this method in your custom checkpoint saver.
+ """
+ raise NotImplementedError
+
+ def list(
+ self,
+ config: RunnableConfig | None,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> Iterator[CheckpointTuple]:
+ """List checkpoints that match the given criteria.
+
+ Args:
+ config: Base configuration for filtering checkpoints.
+ filter: Additional filtering criteria.
+ before: List checkpoints created before this configuration.
+ limit: Maximum number of checkpoints to return.
+
+ Returns:
+ Iterator of matching checkpoint tuples.
+
+ Raises:
+ NotImplementedError: Implement this method in your custom checkpoint saver.
+ """
+ raise NotImplementedError
+
+ def put(
+ self,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ """Store a checkpoint with its configuration and metadata.
+
+ Args:
+ config: Configuration for the checkpoint.
+ checkpoint: The checkpoint to store.
+ metadata: Additional metadata for the checkpoint.
+ new_versions: New channel versions as of this write.
+
+ Returns:
+ RunnableConfig: Updated configuration after storing the checkpoint.
+
+ Raises:
+ NotImplementedError: Implement this method in your custom checkpoint saver.
+ """
+ raise NotImplementedError
+
+ def put_writes(
+ self,
+ config: RunnableConfig,
+ writes: Sequence[tuple[str, Any]],
+ task_id: str,
+ task_path: str = "",
+ ) -> None:
+ """Store intermediate writes linked to a checkpoint.
+
+ Args:
+ config: Configuration of the related checkpoint.
+ writes: List of writes to store.
+ task_id: Identifier for the task creating the writes.
+ task_path: Path of the task creating the writes.
+
+ Raises:
+ NotImplementedError: Implement this method in your custom checkpoint saver.
+ """
+ raise NotImplementedError
+
+ def delete_thread(
+ self,
+ thread_id: str,
+ ) -> None:
+ """Delete all checkpoints and writes associated with a specific thread ID.
+
+ Args:
+ thread_id: The thread ID whose checkpoints should be deleted.
+ """
+ raise NotImplementedError
+
+ async def aget(self, config: RunnableConfig) -> Checkpoint | None:
+ """Asynchronously fetch a checkpoint using the given configuration.
+
+ Args:
+ config: Configuration specifying which checkpoint to retrieve.
+
+ Returns:
+ The requested checkpoint, or `None` if not found.
+ """
+ if value := await self.aget_tuple(config):
+ return value.checkpoint
+
+ async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
+ """Asynchronously fetch a checkpoint tuple using the given configuration.
+
+ Args:
+ config: Configuration specifying which checkpoint to retrieve.
+
+ Returns:
+ The requested checkpoint tuple, or `None` if not found.
+
+ Raises:
+ NotImplementedError: Implement this method in your custom checkpoint saver.
+ """
+ raise NotImplementedError
+
+ async def alist(
+ self,
+ config: RunnableConfig | None,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> AsyncIterator[CheckpointTuple]:
+ """Asynchronously list checkpoints that match the given criteria.
+
+ Args:
+ config: Base configuration for filtering checkpoints.
+ filter: Additional filtering criteria for metadata.
+ before: List checkpoints created before this configuration.
+ limit: Maximum number of checkpoints to return.
+
+ Returns:
+ Async iterator of matching checkpoint tuples.
+
+ Raises:
+ NotImplementedError: Implement this method in your custom checkpoint saver.
+ """
+ raise NotImplementedError
+ yield
+
+ async def aput(
+ self,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ """Asynchronously store a checkpoint with its configuration and metadata.
+
+ Args:
+ config: Configuration for the checkpoint.
+ checkpoint: The checkpoint to store.
+ metadata: Additional metadata for the checkpoint.
+ new_versions: New channel versions as of this write.
+
+ Returns:
+ RunnableConfig: Updated configuration after storing the checkpoint.
+
+ Raises:
+ NotImplementedError: Implement this method in your custom checkpoint saver.
+ """
+ raise NotImplementedError
+
+ async def aput_writes(
+ self,
+ config: RunnableConfig,
+ writes: Sequence[tuple[str, Any]],
+ task_id: str,
+ task_path: str = "",
+ ) -> None:
+ """Asynchronously store intermediate writes linked to a checkpoint.
+
+ Args:
+ config: Configuration of the related checkpoint.
+ writes: List of writes to store.
+ task_id: Identifier for the task creating the writes.
+ task_path: Path of the task creating the writes.
+
+ Raises:
+ NotImplementedError: Implement this method in your custom checkpoint saver.
+ """
+ raise NotImplementedError
+
+ async def adelete_thread(
+ self,
+ thread_id: str,
+ ) -> None:
+ """Delete all checkpoints and writes associated with a specific thread ID.
+
+ Args:
+ thread_id: The thread ID whose checkpoints should be deleted.
+ """
+ raise NotImplementedError
+
+ def get_next_version(self, current: V | None, channel: None) -> V:
+ """Generate the next version ID for a channel.
+
+ Default is to use integer versions, incrementing by `1`.
+
+ If you override, you can use `str`/`int`/`float` versions, as long as they are monotonically increasing.
+
+ Args:
+ current: The current version identifier (`int`, `float`, or `str`).
+ channel: Deprecated argument, kept for backwards compatibility.
+
+ Returns:
+ V: The next version identifier, which must be increasing.
+ """
+ if isinstance(current, str):
+ raise NotImplementedError
+ elif current is None:
+ return 1
+ else:
+ return current + 1
+
+
+class EmptyChannelError(Exception):
+ """Raised when attempting to get the value of a channel that hasn't been updated
+ for the first time yet."""
+
+ pass
+
+
+def get_checkpoint_id(config: RunnableConfig) -> str | None:
+ """Get checkpoint ID."""
+ return config["configurable"].get("checkpoint_id")
+
+
+def get_checkpoint_metadata(
+ config: RunnableConfig, metadata: CheckpointMetadata
+) -> CheckpointMetadata:
+ """Get checkpoint metadata in a backwards-compatible manner."""
+ metadata = {
+ k: v.replace("\u0000", "") if isinstance(v, str) else v
+ for k, v in metadata.items()
+ }
+ for obj in (config.get("metadata"), config.get("configurable")):
+ if not obj:
+ continue
+ for key, v in obj.items():
+ if key in metadata or key in EXCLUDED_METADATA_KEYS or key.startswith("__"):
+ continue
+ elif isinstance(v, str):
+ metadata[key] = v.replace("\u0000", "")
+ elif isinstance(v, (int, bool, float)):
+ metadata[key] = v
+ return metadata
+
+
+def get_serializable_checkpoint_metadata(
+ config: RunnableConfig, metadata: CheckpointMetadata
+) -> CheckpointMetadata:
+ """Get checkpoint metadata in a backwards-compatible manner."""
+ checkpoint_metadata = get_checkpoint_metadata(config, metadata)
+ if "writes" in checkpoint_metadata:
+ checkpoint_metadata.pop("writes")
+ return checkpoint_metadata
+
+
+"""
+Mapping from error type to error index.
+Regular writes just map to their index in the list of writes being saved.
+Special writes (e.g. errors) map to negative indices, to avoid those writes from
+conflicting with regular writes.
+Each Checkpointer implementation should use this mapping in put_writes.
+"""
+WRITES_IDX_MAP = {ERROR: -1, SCHEDULED: -2, INTERRUPT: -3, RESUME: -4}
+
+EXCLUDED_METADATA_KEYS = {
+ "thread_id",
+ "checkpoint_id",
+ "checkpoint_ns",
+ "checkpoint_map",
+ "langgraph_step",
+ "langgraph_node",
+ "langgraph_triggers",
+ "langgraph_path",
+ "langgraph_checkpoint_ns",
+}
+
+# --- below are deprecated utilities used by past versions of LangGraph ---
+
+LATEST_VERSION = 2
+
+
+def empty_checkpoint() -> Checkpoint:
+ from datetime import datetime, timezone
+
+ return Checkpoint(
+ v=LATEST_VERSION,
+ id=str(uuid6(clock_seq=-2)),
+ ts=datetime.now(timezone.utc).isoformat(),
+ channel_values={},
+ channel_versions={},
+ versions_seen={},
+ pending_sends=[],
+ updated_channels=None,
+ )
+
+
+def create_checkpoint(
+ checkpoint: Checkpoint,
+ channels: Mapping[str, ChannelProtocol] | None,
+ step: int,
+ *,
+ id: str | None = None,
+) -> Checkpoint:
+ """Create a checkpoint for the given channels."""
+ from datetime import datetime, timezone
+
+ ts = datetime.now(timezone.utc).isoformat()
+ if channels is None:
+ values = checkpoint["channel_values"]
+ else:
+ values = {}
+ for k, v in channels.items():
+ if k not in checkpoint["channel_versions"]:
+ continue
+ try:
+ values[k] = v.checkpoint()
+ except EmptyChannelError:
+ pass
+ return Checkpoint(
+ v=LATEST_VERSION,
+ ts=ts,
+ id=id or str(uuid6(clock_seq=step)),
+ channel_values=values,
+ channel_versions=checkpoint["channel_versions"],
+ versions_seen=checkpoint["versions_seen"],
+ pending_sends=checkpoint.get("pending_sends", []),
+ updated_channels=None,
+ )
diff --git a/langgraph/source/libs/checkpoint/langgraph/checkpoint/base/id.py b/langgraph/source/libs/checkpoint/langgraph/checkpoint/base/id.py
new file mode 100644
index 0000000000000000000000000000000000000000..30abc48a509601216fe7ddee3be55ceaa6cd93bd
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/checkpoint/base/id.py
@@ -0,0 +1,109 @@
+"""Adapted from
+https://github.com/oittaa/uuid6-python/blob/main/src/uuid6/__init__.py#L95
+Bundled in to avoid install issues with uuid6 package
+"""
+
+from __future__ import annotations
+
+import random
+import time
+import uuid
+
+_last_v6_timestamp = None
+
+
+class UUID(uuid.UUID):
+ r"""UUID draft version objects"""
+
+ __slots__ = ()
+
+ def __init__(
+ self,
+ hex: str | None = None,
+ bytes: bytes | None = None,
+ bytes_le: bytes | None = None,
+ fields: tuple[int, int, int, int, int, int] | None = None,
+ int: int | None = None,
+ version: int | None = None,
+ *,
+ is_safe: uuid.SafeUUID = uuid.SafeUUID.unknown,
+ ) -> None:
+ r"""Create a UUID."""
+
+ if int is None or [hex, bytes, bytes_le, fields].count(None) != 4:
+ return super().__init__(
+ hex=hex,
+ bytes=bytes,
+ bytes_le=bytes_le,
+ fields=fields,
+ int=int,
+ version=version,
+ is_safe=is_safe,
+ )
+ if not 0 <= int < 1 << 128:
+ raise ValueError("int is out of range (need a 128-bit value)")
+ if version is not None:
+ if not 6 <= version <= 8:
+ raise ValueError("illegal version number")
+ # Set the variant to RFC 4122.
+ int &= ~(0xC000 << 48)
+ int |= 0x8000 << 48
+ # Set the version number.
+ int &= ~(0xF000 << 64)
+ int |= version << 76
+ super().__init__(int=int, is_safe=is_safe)
+
+ @property
+ def subsec(self) -> int:
+ return ((self.int >> 64) & 0x0FFF) << 8 | ((self.int >> 54) & 0xFF)
+
+ @property
+ def time(self) -> int:
+ if self.version == 6:
+ return (
+ (self.time_low << 28)
+ | (self.time_mid << 12)
+ | (self.time_hi_version & 0x0FFF)
+ )
+ if self.version == 7:
+ return self.int >> 80
+ if self.version == 8:
+ return (self.int >> 80) * 10**6 + _subsec_decode(self.subsec)
+ return super().time
+
+
+def _subsec_decode(value: int) -> int:
+ return -(-value * 10**6 // 2**20)
+
+
+def uuid6(node: int | None = None, clock_seq: int | None = None) -> UUID:
+ r"""UUID version 6 is a field-compatible version of UUIDv1, reordered for
+ improved DB locality. It is expected that UUIDv6 will primarily be
+ used in contexts where there are existing v1 UUIDs. Systems that do
+ not involve legacy UUIDv1 SHOULD consider using UUIDv7 instead.
+
+ If 'node' is not given, a random 48-bit number is chosen.
+
+ If 'clock_seq' is given, it is used as the sequence number;
+ otherwise a random 14-bit sequence number is chosen."""
+
+ global _last_v6_timestamp
+
+ nanoseconds = time.time_ns()
+ # 0x01b21dd213814000 is the number of 100-ns intervals between the
+ # UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00.
+ timestamp = nanoseconds // 100 + 0x01B21DD213814000
+ if _last_v6_timestamp is not None and timestamp <= _last_v6_timestamp:
+ timestamp = _last_v6_timestamp + 1
+ _last_v6_timestamp = timestamp
+ if clock_seq is None:
+ clock_seq = random.getrandbits(14) # instead of stable storage
+ if node is None:
+ node = random.getrandbits(48)
+ time_high_and_time_mid = (timestamp >> 12) & 0xFFFFFFFFFFFF
+ time_low_and_version = timestamp & 0x0FFF
+ uuid_int = time_high_and_time_mid << 80
+ uuid_int |= time_low_and_version << 64
+ uuid_int |= (clock_seq & 0x3FFF) << 48
+ uuid_int |= node & 0xFFFFFFFFFFFF
+ return UUID(int=uuid_int, version=6)
diff --git a/langgraph/source/libs/checkpoint/langgraph/checkpoint/base/py.typed b/langgraph/source/libs/checkpoint/langgraph/checkpoint/base/py.typed
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/langgraph/source/libs/checkpoint/langgraph/checkpoint/memory/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..da353c2b46f23ab29aadcfdac0f073e3dac800da
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/checkpoint/memory/__init__.py
@@ -0,0 +1,603 @@
+from __future__ import annotations
+
+import logging
+import os
+import pickle
+import random
+import shutil
+from collections import defaultdict
+from collections.abc import AsyncIterator, Iterator, Sequence
+from contextlib import AbstractAsyncContextManager, AbstractContextManager, ExitStack
+from types import TracebackType
+from typing import Any
+
+from langchain_core.runnables import RunnableConfig
+
+from langgraph.checkpoint.base import (
+ WRITES_IDX_MAP,
+ BaseCheckpointSaver,
+ ChannelVersions,
+ Checkpoint,
+ CheckpointMetadata,
+ CheckpointTuple,
+ SerializerProtocol,
+ get_checkpoint_id,
+ get_checkpoint_metadata,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class InMemorySaver(
+ BaseCheckpointSaver[str], AbstractContextManager, AbstractAsyncContextManager
+):
+ """An in-memory checkpoint saver.
+
+ This checkpoint saver stores checkpoints in memory using a `defaultdict`.
+
+ Note:
+ Only use `InMemorySaver` for debugging or testing purposes.
+ For production use cases we recommend installing [langgraph-checkpoint-postgres](https://pypi.org/project/langgraph-checkpoint-postgres/) and using `PostgresSaver` / `AsyncPostgresSaver`.
+
+ If you are using LangSmith Deployment, no checkpointer needs to be specified. The correct managed checkpointer will be used automatically.
+
+ Args:
+ serde: The serializer to use for serializing and deserializing checkpoints.
+
+ Example:
+ ```python
+ import asyncio
+
+ from langgraph.checkpoint.memory import InMemorySaver
+ from langgraph.graph import StateGraph
+
+ builder = StateGraph(int)
+ builder.add_node("add_one", lambda x: x + 1)
+ builder.set_entry_point("add_one")
+ builder.set_finish_point("add_one")
+
+ memory = InMemorySaver()
+ graph = builder.compile(checkpointer=memory)
+ coro = graph.ainvoke(1, {"configurable": {"thread_id": "thread-1"}})
+ asyncio.run(coro) # Output: 2
+ ```
+ """
+
+ # thread ID -> checkpoint NS -> checkpoint ID -> checkpoint mapping
+ storage: defaultdict[
+ str,
+ dict[str, dict[str, tuple[tuple[str, bytes], tuple[str, bytes], str | None]]],
+ ]
+ # (thread ID, checkpoint NS, checkpoint ID) -> (task ID, write idx)
+ writes: defaultdict[
+ tuple[str, str, str],
+ dict[tuple[str, int], tuple[str, str, tuple[str, bytes], str]],
+ ]
+ blobs: dict[
+ tuple[
+ str, str, str, str | int | float
+ ], # thread id, checkpoint ns, channel, version
+ tuple[str, bytes],
+ ]
+
+ def __init__(
+ self,
+ *,
+ serde: SerializerProtocol | None = None,
+ factory: type[defaultdict] = defaultdict,
+ ) -> None:
+ super().__init__(serde=serde)
+ self.storage = factory(lambda: defaultdict(dict))
+ self.writes = factory(dict)
+ self.blobs = factory()
+ self.stack = ExitStack()
+ if factory is not defaultdict:
+ self.stack.enter_context(self.storage) # type: ignore[arg-type]
+ self.stack.enter_context(self.writes) # type: ignore[arg-type]
+ self.stack.enter_context(self.blobs) # type: ignore[arg-type]
+
+ def __enter__(self) -> InMemorySaver:
+ self.stack.__enter__()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> bool | None:
+ return self.stack.__exit__(exc_type, exc_value, traceback)
+
+ async def __aenter__(self) -> InMemorySaver:
+ self.stack.__enter__()
+ return self
+
+ async def __aexit__(
+ self,
+ __exc_type: type[BaseException] | None,
+ __exc_value: BaseException | None,
+ __traceback: TracebackType | None,
+ ) -> bool | None:
+ return self.stack.__exit__(__exc_type, __exc_value, __traceback)
+
+ def _load_blobs(
+ self, thread_id: str, checkpoint_ns: str, versions: ChannelVersions
+ ) -> dict[str, Any]:
+ channel_values: dict[str, Any] = {}
+ for k, v in versions.items():
+ kk = (thread_id, checkpoint_ns, k, v)
+ if kk in self.blobs:
+ vv = self.blobs[kk]
+ if vv[0] != "empty":
+ channel_values[k] = self.serde.loads_typed(vv)
+ return channel_values
+
+ def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
+ """Get a checkpoint tuple from the in-memory storage.
+
+ This method retrieves a checkpoint tuple from the in-memory storage based on the
+ provided config. If the config contains a `checkpoint_id` key, the checkpoint with
+ the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
+ for the given thread ID is retrieved.
+
+ Args:
+ config: The config to use for retrieving the checkpoint.
+
+ Returns:
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
+ """
+ thread_id: str = config["configurable"]["thread_id"]
+ checkpoint_ns: str = config["configurable"].get("checkpoint_ns", "")
+ if checkpoint_id := get_checkpoint_id(config):
+ if saved := self.storage[thread_id][checkpoint_ns].get(checkpoint_id):
+ checkpoint, metadata, parent_checkpoint_id = saved
+ writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values()
+ checkpoint_: Checkpoint = self.serde.loads_typed(checkpoint)
+ return CheckpointTuple(
+ config=config,
+ checkpoint={
+ **checkpoint_,
+ "channel_values": self._load_blobs(
+ thread_id, checkpoint_ns, checkpoint_["channel_versions"]
+ ),
+ },
+ metadata=self.serde.loads_typed(metadata),
+ pending_writes=[
+ (id, c, self.serde.loads_typed(v)) for id, c, v, _ in writes
+ ],
+ parent_config=(
+ {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": parent_checkpoint_id,
+ }
+ }
+ if parent_checkpoint_id
+ else None
+ ),
+ )
+ else:
+ if checkpoints := self.storage[thread_id][checkpoint_ns]:
+ checkpoint_id = max(checkpoints.keys())
+ checkpoint, metadata, parent_checkpoint_id = checkpoints[checkpoint_id]
+ writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values()
+ checkpoint_ = self.serde.loads_typed(checkpoint)
+ return CheckpointTuple(
+ config={
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": checkpoint_id,
+ }
+ },
+ checkpoint={
+ **checkpoint_,
+ "channel_values": self._load_blobs(
+ thread_id, checkpoint_ns, checkpoint_["channel_versions"]
+ ),
+ },
+ metadata=self.serde.loads_typed(metadata),
+ pending_writes=[
+ (id, c, self.serde.loads_typed(v)) for id, c, v, _ in writes
+ ],
+ parent_config=(
+ {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": parent_checkpoint_id,
+ }
+ }
+ if parent_checkpoint_id
+ else None
+ ),
+ )
+
+ def list(
+ self,
+ config: RunnableConfig | None,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> Iterator[CheckpointTuple]:
+ """List checkpoints from the in-memory storage.
+
+ This method retrieves a list of checkpoint tuples from the in-memory storage based
+ on the provided criteria.
+
+ Args:
+ config: Base configuration for filtering checkpoints.
+ filter: Additional filtering criteria for metadata.
+ before: List checkpoints created before this configuration.
+ limit: Maximum number of checkpoints to return.
+
+ Yields:
+ An iterator of matching checkpoint tuples.
+ """
+ thread_ids = (config["configurable"]["thread_id"],) if config else self.storage
+ config_checkpoint_ns = (
+ config["configurable"].get("checkpoint_ns") if config else None
+ )
+ config_checkpoint_id = get_checkpoint_id(config) if config else None
+ for thread_id in thread_ids:
+ for checkpoint_ns in self.storage[thread_id].keys():
+ if (
+ config_checkpoint_ns is not None
+ and checkpoint_ns != config_checkpoint_ns
+ ):
+ continue
+
+ for checkpoint_id, (
+ checkpoint,
+ metadata_b,
+ parent_checkpoint_id,
+ ) in sorted(
+ self.storage[thread_id][checkpoint_ns].items(),
+ key=lambda x: x[0],
+ reverse=True,
+ ):
+ # filter by checkpoint ID from config
+ if config_checkpoint_id and checkpoint_id != config_checkpoint_id:
+ continue
+
+ # filter by checkpoint ID from `before` config
+ if (
+ before
+ and (before_checkpoint_id := get_checkpoint_id(before))
+ and checkpoint_id >= before_checkpoint_id
+ ):
+ continue
+
+ # filter by metadata
+ metadata = self.serde.loads_typed(metadata_b)
+ if filter and not all(
+ query_value == metadata.get(query_key)
+ for query_key, query_value in filter.items()
+ ):
+ continue
+
+ # limit search results
+ if limit is not None and limit <= 0:
+ break
+ elif limit is not None:
+ limit -= 1
+
+ writes = self.writes[
+ (thread_id, checkpoint_ns, checkpoint_id)
+ ].values()
+
+ checkpoint_: Checkpoint = self.serde.loads_typed(checkpoint)
+
+ yield CheckpointTuple(
+ config={
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": checkpoint_id,
+ }
+ },
+ checkpoint={
+ **checkpoint_,
+ "channel_values": self._load_blobs(
+ thread_id,
+ checkpoint_ns,
+ checkpoint_["channel_versions"],
+ ),
+ },
+ metadata=metadata,
+ parent_config=(
+ {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": parent_checkpoint_id,
+ }
+ }
+ if parent_checkpoint_id
+ else None
+ ),
+ pending_writes=[
+ (id, c, self.serde.loads_typed(v)) for id, c, v, _ in writes
+ ],
+ )
+
+ def put(
+ self,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ """Save a checkpoint to the in-memory storage.
+
+ This method saves a checkpoint to the in-memory storage. The checkpoint is associated
+ with the provided config.
+
+ Args:
+ config: The config to associate with the checkpoint.
+ checkpoint: The checkpoint to save.
+ metadata: Additional metadata to save with the checkpoint.
+ new_versions: New versions as of this write
+
+ Returns:
+ RunnableConfig: The updated config containing the saved checkpoint's timestamp.
+ """
+ c = checkpoint.copy()
+ thread_id = config["configurable"]["thread_id"]
+ checkpoint_ns = config["configurable"]["checkpoint_ns"]
+ values: dict[str, Any] = c.pop("channel_values") # type: ignore[misc]
+ for k, v in new_versions.items():
+ self.blobs[(thread_id, checkpoint_ns, k, v)] = (
+ self.serde.dumps_typed(values[k]) if k in values else ("empty", b"")
+ )
+ self.storage[thread_id][checkpoint_ns].update(
+ {
+ checkpoint["id"]: (
+ self.serde.dumps_typed(c),
+ self.serde.dumps_typed(get_checkpoint_metadata(config, metadata)),
+ config["configurable"].get("checkpoint_id"), # parent
+ )
+ }
+ )
+ return {
+ "configurable": {
+ "thread_id": thread_id,
+ "checkpoint_ns": checkpoint_ns,
+ "checkpoint_id": checkpoint["id"],
+ }
+ }
+
+ def put_writes(
+ self,
+ config: RunnableConfig,
+ writes: Sequence[tuple[str, Any]],
+ task_id: str,
+ task_path: str = "",
+ ) -> None:
+ """Save a list of writes to the in-memory storage.
+
+ This method saves a list of writes to the in-memory storage. The writes are associated
+ with the provided config.
+
+ Args:
+ config: The config to associate with the writes.
+ writes: The writes to save.
+ task_id: Identifier for the task creating the writes.
+ task_path: Path of the task creating the writes.
+
+ Returns:
+ RunnableConfig: The updated config containing the saved writes' timestamp.
+ """
+ thread_id = config["configurable"]["thread_id"]
+ checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
+ checkpoint_id = config["configurable"]["checkpoint_id"]
+ outer_key = (thread_id, checkpoint_ns, checkpoint_id)
+ outer_writes_ = self.writes.get(outer_key)
+ for idx, (c, v) in enumerate(writes):
+ inner_key = (task_id, WRITES_IDX_MAP.get(c, idx))
+ if inner_key[1] >= 0 and outer_writes_ and inner_key in outer_writes_:
+ continue
+
+ self.writes[outer_key][inner_key] = (
+ task_id,
+ c,
+ self.serde.dumps_typed(v),
+ task_path,
+ )
+
+ def delete_thread(self, thread_id: str) -> None:
+ """Delete all checkpoints and writes associated with a thread ID.
+
+ Args:
+ thread_id: The thread ID to delete.
+
+ Returns:
+ None
+ """
+ if thread_id in self.storage:
+ del self.storage[thread_id]
+ for k in list(self.writes.keys()):
+ if k[0] == thread_id:
+ del self.writes[k]
+ for k in list(self.blobs.keys()):
+ if k[0] == thread_id:
+ del self.blobs[k]
+
+ async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
+ """Asynchronous version of `get_tuple`.
+
+ This method is an asynchronous wrapper around `get_tuple` that runs the synchronous
+ method in a separate thread using asyncio.
+
+ Args:
+ config: The config to use for retrieving the checkpoint.
+
+ Returns:
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
+ """
+ return self.get_tuple(config)
+
+ async def alist(
+ self,
+ config: RunnableConfig | None,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> AsyncIterator[CheckpointTuple]:
+ """Asynchronous version of `list`.
+
+ This method is an asynchronous wrapper around `list` that runs the synchronous
+ method in a separate thread using asyncio.
+
+ Args:
+ config: The config to use for listing the checkpoints.
+
+ Yields:
+ An asynchronous iterator of checkpoint tuples.
+ """
+ for item in self.list(config, filter=filter, before=before, limit=limit):
+ yield item
+
+ async def aput(
+ self,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ """Asynchronous version of `put`.
+
+ Args:
+ config: The config to associate with the checkpoint.
+ checkpoint: The checkpoint to save.
+ metadata: Additional metadata to save with the checkpoint.
+ new_versions: New versions as of this write
+
+ Returns:
+ RunnableConfig: The updated config containing the saved checkpoint's timestamp.
+ """
+ return self.put(config, checkpoint, metadata, new_versions)
+
+ async def aput_writes(
+ self,
+ config: RunnableConfig,
+ writes: Sequence[tuple[str, Any]],
+ task_id: str,
+ task_path: str = "",
+ ) -> None:
+ """Asynchronous version of `put_writes`.
+
+ This method is an asynchronous wrapper around `put_writes` that runs the synchronous
+ method in a separate thread using asyncio.
+
+ Args:
+ config: The config to associate with the writes.
+ writes: The writes to save, each as a (channel, value) pair.
+ task_id: Identifier for the task creating the writes.
+ task_path: Path of the task creating the writes.
+
+ Returns:
+ None
+ """
+ return self.put_writes(config, writes, task_id, task_path)
+
+ async def adelete_thread(self, thread_id: str) -> None:
+ """Delete all checkpoints and writes associated with a thread ID.
+
+ Args:
+ thread_id: The thread ID to delete.
+
+ Returns:
+ None
+ """
+ return self.delete_thread(thread_id)
+
+ def get_next_version(self, current: str | None, channel: None) -> str:
+ if current is None:
+ current_v = 0
+ elif isinstance(current, int):
+ current_v = current
+ else:
+ current_v = int(current.split(".")[0])
+ next_v = current_v + 1
+ next_h = random.random()
+ return f"{next_v:032}.{next_h:016}"
+
+
+MemorySaver = InMemorySaver # Kept for backwards compatibility
+
+
+class PersistentDict(defaultdict):
+ """Persistent dictionary with an API compatible with shelve and anydbm.
+
+ The dict is kept in memory, so the dictionary operations run as fast as
+ a regular dictionary.
+
+ Write to disk is delayed until close or sync (similar to gdbm's fast mode).
+
+ Input file format is automatically discovered.
+ Output file format is selectable between pickle, json, and csv.
+ All three serialization formats are backed by fast C implementations.
+
+ Adapted from https://code.activestate.com/recipes/576642-persistent-dict-with-multiple-standard-file-format/
+
+ """
+
+ def __init__(self, *args: Any, filename: str, **kwds: Any) -> None:
+ self.flag = "c" # r=readonly, c=create, or n=new
+ self.mode = None # None or an octal triple like 0644
+ self.format = "pickle" # 'csv', 'json', or 'pickle'
+ self.filename = filename
+ super().__init__(*args, **kwds)
+
+ def sync(self) -> None:
+ "Write dict to disk"
+ if self.flag == "r":
+ return
+ tempname = self.filename + ".tmp"
+ fileobj = open(tempname, "wb" if self.format == "pickle" else "w")
+ try:
+ self.dump(fileobj)
+ except Exception:
+ os.remove(tempname)
+ raise
+ finally:
+ fileobj.close()
+ shutil.move(tempname, self.filename) # atomic commit
+ if self.mode is not None:
+ os.chmod(self.filename, self.mode)
+
+ def close(self) -> None:
+ self.sync()
+ self.clear()
+
+ def __enter__(self) -> PersistentDict:
+ return self
+
+ def __exit__(self, *exc_info: Any) -> None:
+ self.close()
+
+ def dump(self, fileobj: Any) -> None:
+ if self.format == "pickle":
+ pickle.dump(dict(self), fileobj, 2)
+ else:
+ raise NotImplementedError("Unknown format: " + repr(self.format))
+
+ def load(self) -> None:
+ # try formats from most restrictive to least restrictive
+ if self.flag == "n":
+ return
+ with open(self.filename, "rb" if self.format == "pickle" else "r") as fileobj:
+ for loader in (pickle.load,):
+ fileobj.seek(0)
+ try:
+ return self.update(loader(fileobj))
+ except EOFError:
+ return
+ except Exception:
+ logger.error(f"Failed to load file: {fileobj.name}")
+ raise
+ raise ValueError("File not in a supported format")
diff --git a/langgraph/source/libs/checkpoint/langgraph/checkpoint/memory/py.typed b/langgraph/source/libs/checkpoint/langgraph/checkpoint/memory/py.typed
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/__init__.py b/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/base.py b/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..186e162fa76eb9cbabca88419673996a07453b28
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/base.py
@@ -0,0 +1,64 @@
+from __future__ import annotations
+
+from typing import Any, Protocol, runtime_checkable
+
+
+class UntypedSerializerProtocol(Protocol):
+ """Protocol for serialization and deserialization of objects."""
+
+ def dumps(self, obj: Any) -> bytes: ...
+
+ def loads(self, data: bytes) -> Any: ...
+
+
+@runtime_checkable
+class SerializerProtocol(Protocol):
+ """Protocol for serialization and deserialization of objects.
+
+ - `dumps_typed`: Serialize an object to a tuple `(type, bytes)`.
+ - `loads_typed`: Deserialize an object from a tuple `(type, bytes)`.
+
+ Valid implementations include the `pickle`, `json` and `orjson` modules.
+ """
+
+ def dumps_typed(self, obj: Any) -> tuple[str, bytes]: ...
+
+ def loads_typed(self, data: tuple[str, bytes]) -> Any: ...
+
+
+class SerializerCompat(SerializerProtocol):
+ def __init__(self, serde: UntypedSerializerProtocol) -> None:
+ self.serde = serde
+
+ def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
+ return type(obj).__name__, self.serde.dumps(obj)
+
+ def loads_typed(self, data: tuple[str, bytes]) -> Any:
+ return self.serde.loads(data[1])
+
+
+def maybe_add_typed_methods(
+ serde: SerializerProtocol | UntypedSerializerProtocol,
+) -> SerializerProtocol:
+ """Wrap serde old serde implementations in a class with loads_typed and dumps_typed for backwards compatibility."""
+
+ if not isinstance(serde, SerializerProtocol):
+ return SerializerCompat(serde)
+
+ return serde
+
+
+class CipherProtocol(Protocol):
+ """Protocol for encryption and decryption of data.
+
+ - `encrypt`: Encrypt plaintext.
+ - `decrypt`: Decrypt ciphertext.
+ """
+
+ def encrypt(self, plaintext: bytes) -> tuple[str, bytes]:
+ """Encrypt plaintext. Returns a tuple `(cipher name, ciphertext)`."""
+ ...
+
+ def decrypt(self, ciphername: str, ciphertext: bytes) -> bytes:
+ """Decrypt ciphertext. Returns the plaintext."""
+ ...
diff --git a/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/encrypted.py b/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/encrypted.py
new file mode 100644
index 0000000000000000000000000000000000000000..829c7dd2afe9f6b4cf2f761ff9ff4cc61d61fd3b
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/encrypted.py
@@ -0,0 +1,80 @@
+import os
+from typing import Any
+
+from langgraph.checkpoint.serde.base import CipherProtocol, SerializerProtocol
+from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
+
+
+class EncryptedSerializer(SerializerProtocol):
+ """Serializer that encrypts and decrypts data using an encryption protocol."""
+
+ def __init__(
+ self, cipher: CipherProtocol, serde: SerializerProtocol = JsonPlusSerializer()
+ ) -> None:
+ self.cipher = cipher
+ self.serde = serde
+
+ def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
+ """Serialize an object to a tuple `(type, bytes)` and encrypt the bytes."""
+ # serialize data
+ typ, data = self.serde.dumps_typed(obj)
+ # encrypt data
+ ciphername, ciphertext = self.cipher.encrypt(data)
+ # add cipher name to type
+ return f"{typ}+{ciphername}", ciphertext
+
+ def loads_typed(self, data: tuple[str, bytes]) -> Any:
+ enc_cipher, ciphertext = data
+ # unencrypted data
+ if "+" not in enc_cipher:
+ return self.serde.loads_typed(data)
+ # extract cipher name
+ typ, ciphername = enc_cipher.split("+", 1)
+ # decrypt data
+ decrypted_data = self.cipher.decrypt(ciphername, ciphertext)
+ # deserialize data
+ return self.serde.loads_typed((typ, decrypted_data))
+
+ @classmethod
+ def from_pycryptodome_aes(
+ cls, serde: SerializerProtocol = JsonPlusSerializer(), **kwargs: Any
+ ) -> "EncryptedSerializer":
+ """Create an `EncryptedSerializer` using AES encryption."""
+ try:
+ from Crypto.Cipher import AES # type: ignore
+ except ImportError:
+ raise ImportError(
+ "Pycryptodome is not installed. Please install it with `pip install pycryptodome`."
+ ) from None
+
+ # check if AES key is provided
+ if "key" in kwargs:
+ key: bytes = kwargs.pop("key")
+ else:
+ key_str = os.getenv("LANGGRAPH_AES_KEY")
+ if key_str is None:
+ raise ValueError("LANGGRAPH_AES_KEY environment variable is not set.")
+ key = key_str.encode()
+ if len(key) not in (16, 24, 32):
+ raise ValueError("LANGGRAPH_AES_KEY must be 16, 24, or 32 bytes long.")
+
+ # set default mode to EAX if not provided
+ if kwargs.get("mode") is None:
+ kwargs["mode"] = AES.MODE_EAX
+
+ class PycryptodomeAesCipher(CipherProtocol):
+ def encrypt(self, plaintext: bytes) -> tuple[str, bytes]:
+ cipher = AES.new(key, **kwargs)
+ ciphertext, tag = cipher.encrypt_and_digest(plaintext)
+ return "aes", cipher.nonce + tag + ciphertext
+
+ def decrypt(self, ciphername: str, ciphertext: bytes) -> bytes:
+ assert ciphername == "aes", f"Unsupported cipher: {ciphername}"
+ nonce = ciphertext[:16]
+ tag = ciphertext[16:32]
+ actual_ciphertext = ciphertext[32:]
+
+ cipher = AES.new(key, **kwargs, nonce=nonce)
+ return cipher.decrypt_and_verify(actual_ciphertext, tag)
+
+ return cls(PycryptodomeAesCipher(), serde)
diff --git a/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py b/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py
new file mode 100644
index 0000000000000000000000000000000000000000..c4b5503088db430a954e061527db5242f134fff9
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py
@@ -0,0 +1,650 @@
+from __future__ import annotations
+
+import dataclasses
+import decimal
+import importlib
+import json
+import logging
+import pathlib
+import pickle
+import re
+import sys
+from collections import deque
+from collections.abc import Callable, Sequence
+from datetime import date, datetime, time, timedelta, timezone
+from enum import Enum
+from inspect import isclass
+from ipaddress import (
+ IPv4Address,
+ IPv4Interface,
+ IPv4Network,
+ IPv6Address,
+ IPv6Interface,
+ IPv6Network,
+)
+from typing import Any, Literal
+from uuid import UUID
+from zoneinfo import ZoneInfo
+
+import ormsgpack
+from langchain_core.load.load import Reviver
+
+from langgraph.checkpoint.serde.base import SerializerProtocol
+from langgraph.checkpoint.serde.types import SendProtocol
+from langgraph.store.base import Item
+
+LC_REVIVER = Reviver()
+EMPTY_BYTES = b""
+logger = logging.getLogger(__name__)
+
+
+class JsonPlusSerializer(SerializerProtocol):
+ """Serializer that uses ormsgpack, with optional fallbacks.
+
+ !!! warning
+
+ Security note: This serializer is intended for use within the `BaseCheckpointSaver`
+ class and called within the Pregel loop. It should not be used on untrusted
+ python objects. If an attacker can write directly to your checkpoint database,
+ they may be able to trigger code execution when data is deserialized.
+ """
+
+ def __init__(
+ self,
+ *,
+ pickle_fallback: bool = False,
+ allowed_json_modules: Sequence[tuple[str, ...]] | Literal[True] | None = None,
+ __unpack_ext_hook__: Callable[[int, bytes], Any] | None = None,
+ ) -> None:
+ self.pickle_fallback = pickle_fallback
+ self._allowed_modules = (
+ {mod_and_name for mod_and_name in allowed_json_modules}
+ if allowed_json_modules and allowed_json_modules is not True
+ else (allowed_json_modules if allowed_json_modules is True else None)
+ )
+ self._unpack_ext_hook = (
+ __unpack_ext_hook__
+ if __unpack_ext_hook__ is not None
+ else _msgpack_ext_hook
+ )
+
+ def _encode_constructor_args(
+ self,
+ constructor: Callable | type[Any],
+ *,
+ method: None | str | Sequence[None | str] = None,
+ args: Sequence[Any] | None = None,
+ kwargs: dict[str, Any] | None = None,
+ ) -> dict[str, Any]:
+ out = {
+ "lc": 2,
+ "type": "constructor",
+ "id": (*constructor.__module__.split("."), constructor.__name__),
+ }
+ if method is not None:
+ out["method"] = method
+ if args is not None:
+ out["args"] = args
+ if kwargs is not None:
+ out["kwargs"] = kwargs
+ return out
+
+ def _reviver(self, value: dict[str, Any]) -> Any:
+ if self._allowed_modules and (
+ value.get("lc", None) == 2
+ and value.get("type", None) == "constructor"
+ and value.get("id", None) is not None
+ ):
+ try:
+ return self._revive_lc2(value)
+ except InvalidModuleError as e:
+ logger.warning(
+ "Object %s is not in the deserialization allowlist.\n%s",
+ value["id"],
+ e.message,
+ )
+
+ return LC_REVIVER(value)
+
+ def _revive_lc2(self, value: dict[str, Any]) -> Any:
+ self._check_allowed_modules(value)
+
+ [*module, name] = value["id"]
+ try:
+ mod = importlib.import_module(".".join(module))
+ cls = getattr(mod, name)
+ method = value.get("method")
+ if isinstance(method, str):
+ methods = [getattr(cls, method)]
+ elif isinstance(method, list):
+ methods = [cls if m is None else getattr(cls, m) for m in method]
+ else:
+ methods = [cls]
+ args = value.get("args")
+ kwargs = value.get("kwargs")
+ for method in methods:
+ try:
+ if isclass(method) and issubclass(method, BaseException):
+ return None
+ if args and kwargs:
+ return method(*args, **kwargs)
+ elif args:
+ return method(*args)
+ elif kwargs:
+ return method(**kwargs)
+ else:
+ return method()
+ except Exception:
+ continue
+ except Exception:
+ return None
+
+ def _check_allowed_modules(self, value: dict[str, Any]) -> None:
+ needed = tuple(value["id"])
+ method = value.get("method")
+ if isinstance(method, list):
+ method_display = ",".join(m or "" for m in method)
+ elif isinstance(method, str):
+ method_display = method
+ else:
+ method_display = ""
+
+ dotted = ".".join(needed)
+ if not self._allowed_modules:
+ raise InvalidModuleError(
+ f"Refused to deserialize JSON constructor: {dotted} (method: {method_display}). "
+ "No allowed_json_modules configured.\n\n"
+ "Unblock with ONE of:\n"
+ f" • JsonPlusSerializer(allowed_json_modules=[{needed!r}, ...])\n"
+ " • (DANGEROUS) JsonPlusSerializer(allowed_json_modules=True)\n\n"
+ "Note: Prefix allowlists are intentionally unsupported; prefer exact symbols "
+ "or plain-JSON representations revived without import-time side effects."
+ )
+
+ if self._allowed_modules is True:
+ return
+ if needed in self._allowed_modules:
+ return
+
+ raise InvalidModuleError(
+ f"Refused to deserialize JSON constructor: {dotted} (method: {method_display}). "
+ "Symbol is not in the deserialization allowlist.\n\n"
+ "Add exactly this symbol to unblock:\n"
+ f" JsonPlusSerializer(allowed_json_modules=[{needed!r}, ...])\n"
+ "Or, as a last resort (DANGEROUS):\n"
+ " JsonPlusSerializer(allowed_json_modules=True)"
+ )
+
+ def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
+ if obj is None:
+ return "null", EMPTY_BYTES
+ elif isinstance(obj, bytes):
+ return "bytes", obj
+ elif isinstance(obj, bytearray):
+ return "bytearray", obj
+ else:
+ try:
+ return "msgpack", _msgpack_enc(obj)
+ except ormsgpack.MsgpackEncodeError as exc:
+ if self.pickle_fallback:
+ return "pickle", pickle.dumps(obj)
+ raise exc
+
+ def loads_typed(self, data: tuple[str, bytes]) -> Any:
+ type_, data_ = data
+ if type_ == "null":
+ return None
+ elif type_ == "bytes":
+ return data_
+ elif type_ == "bytearray":
+ return bytearray(data_)
+ elif type_ == "json":
+ return json.loads(data_, object_hook=self._reviver)
+ elif type_ == "msgpack":
+ return ormsgpack.unpackb(
+ data_, ext_hook=self._unpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
+ )
+ elif self.pickle_fallback and type_ == "pickle":
+ return pickle.loads(data_)
+ else:
+ raise NotImplementedError(f"Unknown serialization type: {type_}")
+
+
+# --- msgpack ---
+
+EXT_CONSTRUCTOR_SINGLE_ARG = 0
+EXT_CONSTRUCTOR_POS_ARGS = 1
+EXT_CONSTRUCTOR_KW_ARGS = 2
+EXT_METHOD_SINGLE_ARG = 3
+EXT_PYDANTIC_V1 = 4
+EXT_PYDANTIC_V2 = 5
+EXT_NUMPY_ARRAY = 6
+
+
+def _msgpack_default(obj: Any) -> str | ormsgpack.Ext:
+ if hasattr(obj, "model_dump") and callable(obj.model_dump): # pydantic v2
+ return ormsgpack.Ext(
+ EXT_PYDANTIC_V2,
+ _msgpack_enc(
+ (
+ obj.__class__.__module__,
+ obj.__class__.__name__,
+ obj.model_dump(),
+ "model_validate_json",
+ ),
+ ),
+ )
+ elif hasattr(obj, "get_secret_value") and callable(obj.get_secret_value):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_SINGLE_ARG,
+ _msgpack_enc(
+ (
+ obj.__class__.__module__,
+ obj.__class__.__name__,
+ obj.get_secret_value(),
+ ),
+ ),
+ )
+ elif hasattr(obj, "dict") and callable(obj.dict): # pydantic v1
+ return ormsgpack.Ext(
+ EXT_PYDANTIC_V1,
+ _msgpack_enc(
+ (
+ obj.__class__.__module__,
+ obj.__class__.__name__,
+ obj.dict(),
+ ),
+ ),
+ )
+ elif hasattr(obj, "_asdict") and callable(obj._asdict): # namedtuple
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_KW_ARGS,
+ _msgpack_enc(
+ (
+ obj.__class__.__module__,
+ obj.__class__.__name__,
+ obj._asdict(),
+ ),
+ ),
+ )
+ elif isinstance(obj, pathlib.Path):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_POS_ARGS,
+ _msgpack_enc(
+ (obj.__class__.__module__, obj.__class__.__name__, obj.parts),
+ ),
+ )
+ elif isinstance(obj, re.Pattern):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_POS_ARGS,
+ _msgpack_enc(
+ ("re", "compile", (obj.pattern, obj.flags)),
+ ),
+ )
+ elif isinstance(obj, UUID):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_SINGLE_ARG,
+ _msgpack_enc(
+ (obj.__class__.__module__, obj.__class__.__name__, obj.hex),
+ ),
+ )
+ elif isinstance(obj, decimal.Decimal):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_SINGLE_ARG,
+ _msgpack_enc(
+ (obj.__class__.__module__, obj.__class__.__name__, str(obj)),
+ ),
+ )
+ elif isinstance(obj, (set, frozenset, deque)):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_SINGLE_ARG,
+ _msgpack_enc(
+ (obj.__class__.__module__, obj.__class__.__name__, tuple(obj)),
+ ),
+ )
+ elif isinstance(obj, (IPv4Address, IPv4Interface, IPv4Network)):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_SINGLE_ARG,
+ _msgpack_enc(
+ (obj.__class__.__module__, obj.__class__.__name__, str(obj)),
+ ),
+ )
+ elif isinstance(obj, (IPv6Address, IPv6Interface, IPv6Network)):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_SINGLE_ARG,
+ _msgpack_enc(
+ (obj.__class__.__module__, obj.__class__.__name__, str(obj)),
+ ),
+ )
+ elif isinstance(obj, datetime):
+ return ormsgpack.Ext(
+ EXT_METHOD_SINGLE_ARG,
+ _msgpack_enc(
+ (
+ obj.__class__.__module__,
+ obj.__class__.__name__,
+ obj.isoformat(),
+ "fromisoformat",
+ ),
+ ),
+ )
+ elif isinstance(obj, timedelta):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_POS_ARGS,
+ _msgpack_enc(
+ (
+ obj.__class__.__module__,
+ obj.__class__.__name__,
+ (obj.days, obj.seconds, obj.microseconds),
+ ),
+ ),
+ )
+ elif isinstance(obj, date):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_POS_ARGS,
+ _msgpack_enc(
+ (
+ obj.__class__.__module__,
+ obj.__class__.__name__,
+ (obj.year, obj.month, obj.day),
+ ),
+ ),
+ )
+ elif isinstance(obj, time):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_KW_ARGS,
+ _msgpack_enc(
+ (
+ obj.__class__.__module__,
+ obj.__class__.__name__,
+ {
+ "hour": obj.hour,
+ "minute": obj.minute,
+ "second": obj.second,
+ "microsecond": obj.microsecond,
+ "tzinfo": obj.tzinfo,
+ "fold": obj.fold,
+ },
+ ),
+ ),
+ )
+ elif isinstance(obj, timezone):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_POS_ARGS,
+ _msgpack_enc(
+ (
+ obj.__class__.__module__,
+ obj.__class__.__name__,
+ obj.__getinitargs__(), # type: ignore[attr-defined]
+ ),
+ ),
+ )
+ elif isinstance(obj, ZoneInfo):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_SINGLE_ARG,
+ _msgpack_enc(
+ (obj.__class__.__module__, obj.__class__.__name__, obj.key),
+ ),
+ )
+ elif isinstance(obj, Enum):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_SINGLE_ARG,
+ _msgpack_enc(
+ (obj.__class__.__module__, obj.__class__.__name__, obj.value),
+ ),
+ )
+ elif isinstance(obj, SendProtocol):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_POS_ARGS,
+ _msgpack_enc(
+ (obj.__class__.__module__, obj.__class__.__name__, (obj.node, obj.arg)),
+ ),
+ )
+ elif dataclasses.is_dataclass(obj):
+ # doesn't use dataclasses.asdict to avoid deepcopy and recursion
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_KW_ARGS,
+ _msgpack_enc(
+ (
+ obj.__class__.__module__,
+ obj.__class__.__name__,
+ {
+ field.name: getattr(obj, field.name)
+ for field in dataclasses.fields(obj)
+ },
+ ),
+ ),
+ )
+ elif isinstance(obj, Item):
+ return ormsgpack.Ext(
+ EXT_CONSTRUCTOR_KW_ARGS,
+ _msgpack_enc(
+ (
+ obj.__class__.__module__,
+ obj.__class__.__name__,
+ {k: getattr(obj, k) for k in obj.__slots__},
+ ),
+ ),
+ )
+ elif (np_mod := sys.modules.get("numpy")) is not None and isinstance(
+ obj, np_mod.ndarray
+ ):
+ order = "F" if obj.flags.f_contiguous and not obj.flags.c_contiguous else "C"
+ if obj.flags.c_contiguous:
+ mv = memoryview(obj)
+ try:
+ meta = (obj.dtype.str, obj.shape, order, mv)
+ return ormsgpack.Ext(EXT_NUMPY_ARRAY, _msgpack_enc(meta))
+ finally:
+ mv.release()
+ else:
+ buf = obj.tobytes(order="A")
+ meta = (obj.dtype.str, obj.shape, order, buf)
+ return ormsgpack.Ext(EXT_NUMPY_ARRAY, _msgpack_enc(meta))
+
+ elif isinstance(obj, BaseException):
+ return repr(obj)
+ else:
+ raise TypeError(f"Object of type {obj.__class__.__name__} is not serializable")
+
+
+def _msgpack_ext_hook(code: int, data: bytes) -> Any:
+ if code == EXT_CONSTRUCTOR_SINGLE_ARG:
+ try:
+ tup = ormsgpack.unpackb(
+ data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
+ )
+ # module, name, arg
+ return getattr(importlib.import_module(tup[0]), tup[1])(tup[2])
+ except Exception:
+ return
+ elif code == EXT_CONSTRUCTOR_POS_ARGS:
+ try:
+ tup = ormsgpack.unpackb(
+ data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
+ )
+ # module, name, args
+ return getattr(importlib.import_module(tup[0]), tup[1])(*tup[2])
+ except Exception:
+ return
+ elif code == EXT_CONSTRUCTOR_KW_ARGS:
+ try:
+ tup = ormsgpack.unpackb(
+ data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
+ )
+ # module, name, args
+ return getattr(importlib.import_module(tup[0]), tup[1])(**tup[2])
+ except Exception:
+ return
+ elif code == EXT_METHOD_SINGLE_ARG:
+ try:
+ tup = ormsgpack.unpackb(
+ data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
+ )
+ # module, name, arg, method
+ return getattr(getattr(importlib.import_module(tup[0]), tup[1]), tup[3])(
+ tup[2]
+ )
+ except Exception:
+ return
+ elif code == EXT_PYDANTIC_V1:
+ try:
+ tup = ormsgpack.unpackb(
+ data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
+ )
+ # module, name, kwargs
+ cls = getattr(importlib.import_module(tup[0]), tup[1])
+ try:
+ return cls(**tup[2])
+ except Exception:
+ return cls.construct(**tup[2])
+ except Exception:
+ # for pydantic objects we can't find/reconstruct
+ # let's return the kwargs dict instead
+ try:
+ return tup[2]
+ except NameError:
+ return
+ elif code == EXT_PYDANTIC_V2:
+ try:
+ tup = ormsgpack.unpackb(
+ data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
+ )
+ # module, name, kwargs, method
+ cls = getattr(importlib.import_module(tup[0]), tup[1])
+ try:
+ return cls(**tup[2])
+ except Exception:
+ return cls.model_construct(**tup[2])
+ except Exception:
+ # for pydantic objects we can't find/reconstruct
+ # let's return the kwargs dict instead
+ try:
+ return tup[2]
+ except NameError:
+ return
+ elif code == EXT_NUMPY_ARRAY:
+ try:
+ import numpy as _np
+
+ dtype_str, shape, order, buf = ormsgpack.unpackb(
+ data, ext_hook=_msgpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
+ )
+ arr = _np.frombuffer(buf, dtype=_np.dtype(dtype_str))
+ return arr.reshape(shape, order=order)
+ except Exception:
+ return
+
+
+def _msgpack_ext_hook_to_json(code: int, data: bytes) -> Any:
+ if code == EXT_CONSTRUCTOR_SINGLE_ARG:
+ try:
+ tup = ormsgpack.unpackb(
+ data,
+ ext_hook=_msgpack_ext_hook_to_json,
+ option=ormsgpack.OPT_NON_STR_KEYS,
+ )
+ if tup[0] == "uuid" and tup[1] == "UUID":
+ hex_ = tup[2]
+ return (
+ f"{hex_[:8]}-{hex_[8:12]}-{hex_[12:16]}-{hex_[16:20]}-{hex_[20:]}"
+ )
+ # module, name, arg
+ return tup[2]
+ except Exception:
+ return
+ elif code == EXT_CONSTRUCTOR_POS_ARGS:
+ try:
+ tup = ormsgpack.unpackb(
+ data,
+ ext_hook=_msgpack_ext_hook_to_json,
+ option=ormsgpack.OPT_NON_STR_KEYS,
+ )
+ if tup[0] == "langgraph.types" and tup[1] == "Send":
+ from langgraph.types import Send # type: ignore
+
+ return Send(*tup[2])
+ # module, name, args
+ return tup[2]
+ except Exception:
+ return
+ elif code == EXT_CONSTRUCTOR_KW_ARGS:
+ try:
+ tup = ormsgpack.unpackb(
+ data,
+ ext_hook=_msgpack_ext_hook_to_json,
+ option=ormsgpack.OPT_NON_STR_KEYS,
+ )
+ # module, name, args
+ return tup[2]
+ except Exception:
+ return
+ elif code == EXT_METHOD_SINGLE_ARG:
+ try:
+ tup = ormsgpack.unpackb(
+ data,
+ ext_hook=_msgpack_ext_hook_to_json,
+ option=ormsgpack.OPT_NON_STR_KEYS,
+ )
+ # module, name, arg, method
+ return tup[2]
+ except Exception:
+ return
+ elif code == EXT_PYDANTIC_V1:
+ try:
+ tup = ormsgpack.unpackb(
+ data,
+ ext_hook=_msgpack_ext_hook_to_json,
+ option=ormsgpack.OPT_NON_STR_KEYS,
+ )
+ # module, name, kwargs
+ return tup[2]
+ except Exception:
+ # for pydantic objects we can't find/reconstruct
+ # let's return the kwargs dict instead
+ return
+ elif code == EXT_PYDANTIC_V2:
+ try:
+ tup = ormsgpack.unpackb(
+ data,
+ ext_hook=_msgpack_ext_hook_to_json,
+ option=ormsgpack.OPT_NON_STR_KEYS,
+ )
+ # module, name, kwargs, method
+ return tup[2]
+ except Exception:
+ return
+ elif code == EXT_NUMPY_ARRAY:
+ try:
+ import numpy as _np
+
+ dtype_str, shape, order, buf = ormsgpack.unpackb(
+ data,
+ ext_hook=_msgpack_ext_hook_to_json,
+ option=ormsgpack.OPT_NON_STR_KEYS,
+ )
+ arr = _np.frombuffer(buf, dtype=_np.dtype(dtype_str))
+ return arr.reshape(shape, order=order).tolist()
+ except Exception:
+ return
+
+
+class InvalidModuleError(Exception):
+ """Exception raised when a module is not in the allowlist."""
+
+ def __init__(self, message: str):
+ self.message = message
+
+
+_option = (
+ ormsgpack.OPT_NON_STR_KEYS
+ | ormsgpack.OPT_PASSTHROUGH_DATACLASS
+ | ormsgpack.OPT_PASSTHROUGH_DATETIME
+ | ormsgpack.OPT_PASSTHROUGH_ENUM
+ | ormsgpack.OPT_PASSTHROUGH_UUID
+ | ormsgpack.OPT_REPLACE_SURROGATES
+)
+
+
+def _msgpack_enc(data: Any) -> bytes:
+ return ormsgpack.packb(data, default=_msgpack_default, option=_option)
diff --git a/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/py.typed b/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/py.typed
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/types.py b/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/types.py
new file mode 100644
index 0000000000000000000000000000000000000000..65a2b0c8e6142a17effb977ca1c00e91459c89d5
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/checkpoint/serde/types.py
@@ -0,0 +1,51 @@
+from collections.abc import Sequence
+from typing import (
+ Any,
+ Protocol,
+ TypeVar,
+ runtime_checkable,
+)
+
+from typing_extensions import Self
+
+ERROR = "__error__"
+SCHEDULED = "__scheduled__"
+INTERRUPT = "__interrupt__"
+RESUME = "__resume__"
+TASKS = "__pregel_tasks"
+
+Value = TypeVar("Value", covariant=True)
+Update = TypeVar("Update", contravariant=True)
+C = TypeVar("C")
+
+
+class ChannelProtocol(Protocol[Value, Update, C]):
+ # Mirrors langgraph.channels.base.BaseChannel
+ @property
+ def ValueType(self) -> Any: ...
+
+ @property
+ def UpdateType(self) -> Any: ...
+
+ def checkpoint(self) -> C | None: ...
+
+ def from_checkpoint(self, checkpoint: C | None) -> Self: ...
+
+ def update(self, values: Sequence[Update]) -> bool: ...
+
+ def get(self) -> Value: ...
+
+ def consume(self) -> bool: ...
+
+
+@runtime_checkable
+class SendProtocol(Protocol):
+ # Mirrors langgraph.constants.Send
+ node: str
+ arg: Any
+
+ def __hash__(self) -> int: ...
+
+ def __repr__(self) -> str: ...
+
+ def __eq__(self, value: object) -> bool: ...
diff --git a/langgraph/source/libs/checkpoint/langgraph/store/__init__.py b/langgraph/source/libs/checkpoint/langgraph/store/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/store/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/checkpoint/langgraph/store/base/__init__.py b/langgraph/source/libs/checkpoint/langgraph/store/base/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..5b04fb51dcbb3a651769413b45f3d7652d249f95
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/store/base/__init__.py
@@ -0,0 +1,1314 @@
+"""Base classes and types for persistent key-value stores.
+
+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 __future__ import annotations
+
+from abc import ABC, abstractmethod
+from collections.abc import Iterable
+from datetime import datetime
+from typing import (
+ Any,
+ Literal,
+ NamedTuple,
+ TypedDict,
+ cast,
+)
+
+from langchain_core.embeddings import Embeddings
+from typing_extensions import override
+
+from langgraph.store.base.embed import (
+ AEmbeddingsFunc,
+ EmbeddingsFunc,
+ ensure_embeddings,
+ get_text_at_path,
+ tokenize_path,
+)
+
+
+class NotProvided:
+ """Sentinel singleton."""
+
+ def __bool__(self) -> Literal[False]:
+ return False
+
+ @override
+ def __repr__(self) -> str:
+ return "NOT_GIVEN"
+
+
+NOT_PROVIDED = NotProvided()
+
+
+class Item:
+ """Represents a stored item with metadata.
+
+ Args:
+ value: The stored data as a dictionary. Keys are filterable.
+ key: Unique identifier within the namespace.
+ namespace: Hierarchical path defining the collection in which this document resides.
+ Represented as a tuple of strings, allowing for nested categorization.
+ For example: `("documents", 'user123')`
+ created_at: Timestamp of item creation.
+ updated_at: Timestamp of last update.
+ """
+
+ __slots__ = ("value", "key", "namespace", "created_at", "updated_at")
+
+ def __init__(
+ self,
+ *,
+ value: dict[str, Any],
+ key: str,
+ namespace: tuple[str, ...],
+ created_at: datetime,
+ updated_at: datetime,
+ ):
+ self.value = value
+ self.key = key
+ # The casting from json-like types is for if this object is
+ # deserialized.
+ self.namespace = tuple(namespace)
+ self.created_at = (
+ datetime.fromisoformat(cast(str, created_at))
+ if isinstance(created_at, str)
+ else created_at
+ )
+ self.updated_at = (
+ datetime.fromisoformat(cast(str, updated_at))
+ if isinstance(updated_at, str)
+ else updated_at
+ )
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, Item):
+ return False
+ return (
+ self.value == other.value
+ and self.key == other.key
+ and self.namespace == other.namespace
+ and self.created_at == other.created_at
+ and self.updated_at == other.updated_at
+ )
+
+ def __hash__(self) -> int:
+ return hash((self.namespace, self.key))
+
+ def dict(self) -> dict:
+ return {
+ "namespace": list(self.namespace),
+ "key": self.key,
+ "value": self.value,
+ "created_at": self.created_at.isoformat(),
+ "updated_at": self.updated_at.isoformat(),
+ }
+
+ def __repr__(self) -> str:
+ return f"Item({', '.join(f'{k}={v!r}' for k, v in self.dict().items())})"
+
+
+class SearchItem(Item):
+ """Represents an item returned from a search operation with additional metadata."""
+
+ __slots__ = ("score",)
+
+ def __init__(
+ self,
+ namespace: tuple[str, ...],
+ key: str,
+ value: dict[str, Any],
+ created_at: datetime,
+ updated_at: datetime,
+ score: float | None = 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 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 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 for the item within its specific namespace.
+
+ ???+ example "Examples"
+
+ ```python
+ "user123" # For a user profile
+ "doc456" # For a document
+ ```
+ """
+ refresh_ttl: bool = True
+ """Whether to refresh TTLs for the returned item.
+
+ If no TTL was specified for the original item(s),
+ or if TTL support is not enabled for your adapter,
+ this argument is ignored.
+ """
+
+
+class SearchOp(NamedTuple):
+ """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 defining the search scope.
+
+ ???+ example "Examples"
+
+ ```python
+ () # Search entire store
+ ("documents",) # Search all documents
+ ("users", "content") # Search within user content
+ ```
+ """
+
+ filter: dict[str, Any] | None = None
+ """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"
+ }
+ ```
+ """
+
+ limit: int = 10
+ """Maximum number of items to return in the search results."""
+
+ offset: int = 0
+ """Number of matching items to skip for pagination."""
+
+ query: str | None = None
+ """Natural language search query for semantic search capabilities.
+
+ ???+ example "Examples"
+
+ - "technical documentation about REST APIs"
+ - "machine learning papers from 2023"
+ """
+ refresh_ttl: bool = True
+ """Whether to refresh TTLs for the returned item.
+
+ If no TTL was specified for the original item(s),
+ or if TTL support is not enabled for your adapter,
+ this argument is ignored.
+ """
+
+
+# Type representing a namespace path that can include wildcards
+NamespacePath = tuple[str | Literal["*"], ...]
+"""A tuple representing a namespace path that can include wildcards.
+
+???+ 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
+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 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
+ """Type of namespace matching to perform."""
+
+ path: NamespacePath
+ """Namespace path pattern that can include wildcards."""
+
+
+class ListNamespacesOp(NamedTuple):
+ """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: tuple[MatchCondition, ...] | None = None
+ """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: int | None = None
+ """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 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: dict[str, Any] | None
+ """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: Literal[False] | list[str] | None = 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 (whole item)
+ - `list[str]` - List of fields to index
+
+ ```python
+ [
+ "metadata.title", # Nested field access
+ "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
+ "metadata.tags[*]", # All tags in metadata
+ ]
+ ```
+ """
+ ttl: float | None = None
+ """Controls the TTL (time-to-live) for the item in minutes.
+
+ If provided, and if the store you are using supports this feature, the item
+ will expire this many minutes after it was last accessed. The expiration timer
+ refreshes on both read operations (get/search) and write operations (put/update).
+ When the TTL expires, the item will be scheduled for deletion on a best-effort basis.
+ Defaults to `None` (no expiration).
+ """
+
+
+Op = GetOp | SearchOp | PutOp | ListNamespacesOp
+Result = Item | list[Item] | list[SearchItem] | list[tuple[str, ...]] | None
+
+
+class InvalidNamespaceError(ValueError):
+ """Provided namespace is invalid."""
+
+
+class TTLConfig(TypedDict, total=False):
+ """Configuration for TTL (time-to-live) behavior in the store."""
+
+ refresh_on_read: bool
+ """Default behavior for refreshing TTLs on read operations (`GET` and `SEARCH`).
+
+ If `True`, TTLs will be refreshed on read operations (get/search) by default.
+ This can be overridden per-operation by explicitly setting `refresh_ttl`.
+ Defaults to `True` if not configured.
+ """
+ default_ttl: float | None
+ """Default TTL (time-to-live) in minutes for new items.
+
+ If provided, new items will expire after this many minutes after their last access.
+ The expiration timer refreshes on both read and write operations.
+ Defaults to `None` (no expiration).
+ """
+ sweep_interval_minutes: int | None
+ """Interval in minutes between TTL sweep operations.
+
+ If provided, the store will periodically delete expired items based on TTL.
+ Defaults to None (no sweeping).
+ """
+
+
+class IndexConfig(TypedDict, total=False):
+ """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.
+
+ Common embedding models have the following 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`
+ """
+
+ embed: Embeddings | EmbeddingsFunc | AEmbeddingsFunc | str
+ """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`)
+ 4. A provider string (e.g., `"openai:text-embedding-3-small"`)
+
+ ???+ 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: list[str] | None
+ """Fields to extract text from for embedding generation.
+
+ Controls which parts of stored items are embedded for semantic search. Follows JSON path syntax:
+
+ - `["$"]`: 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
+ # 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"`)
+ """
+
+
+class BaseStore(ABC):
+ """Abstract base class 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.
+ 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.
+
+ Similarly, TTL (time-to-live) support is disabled by default.
+ Subclasses must explicitly set `supports_ttl = True` to enable this feature.
+ """
+
+ supports_ttl: bool = False
+ ttl_config: TTLConfig | None = None
+
+ __slots__ = ("__weakref__",)
+
+ @abstractmethod
+ def batch(self, ops: Iterable[Op]) -> list[Result]:
+ """Execute multiple operations synchronously in a single batch.
+
+ Args:
+ ops: An iterable of operations to execute.
+
+ Returns:
+ A list of results, where each result corresponds to an operation in the input.
+ The order of results matches the order of input operations.
+ """
+
+ @abstractmethod
+ async def abatch(self, ops: Iterable[Op]) -> list[Result]:
+ """Execute multiple operations asynchronously in a single batch.
+
+ Args:
+ ops: An iterable of operations to execute.
+
+ Returns:
+ A list of results, where each result corresponds to an operation in the input.
+ The order of results matches the order of input operations.
+ """
+
+ def get(
+ self,
+ namespace: tuple[str, ...],
+ key: str,
+ *,
+ refresh_ttl: bool | None = None,
+ ) -> Item | None:
+ """Retrieve a single item.
+
+ Args:
+ namespace: Hierarchical path for the item.
+ key: Unique identifier within the namespace.
+ refresh_ttl: Whether to refresh TTLs for the returned item.
+ If `None`, uses the store's default `refresh_ttl` setting.
+ If no TTL is specified, this argument is ignored.
+
+ Returns:
+ The retrieved item or `None` if not found.
+ """
+ return self.batch(
+ [GetOp(namespace, str(key), _ensure_refresh(self.ttl_config, refresh_ttl))]
+ )[0]
+
+ def search(
+ self,
+ namespace_prefix: tuple[str, ...],
+ /,
+ *,
+ query: str | None = None,
+ filter: dict[str, Any] | None = None,
+ limit: int = 10,
+ offset: int = 0,
+ refresh_ttl: bool | None = None,
+ ) -> 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.
+ refresh_ttl: Whether to refresh TTLs for the returned items.
+ If no TTL is specified, this argument is ignored.
+
+ 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. Defaults to ["$"]
+ }
+ )
+
+ # 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,
+ _ensure_refresh(self.ttl_config, refresh_ttl),
+ )
+ ]
+ )[0]
+
+ def put(
+ self,
+ namespace: tuple[str, ...],
+ key: str,
+ value: dict[str, Any],
+ index: Literal[False] | list[str] | None = None,
+ *,
+ ttl: float | None | NotProvided = NOT_PROVIDED,
+ ) -> None:
+ """Store or update an item in the store.
+
+ Args:
+ 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 `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"`
+ - Array access: `"chapters[*].content"` (each indexed separately)
+ - Specific indices: `"authors[0].name"`
+ ttl: Time to live in minutes. Support for this argument depends on your store adapter.
+ If specified, the item will expire after this many minutes from when it was last accessed.
+ None means no expiration. Expired runs will be deleted opportunistically.
+ By default, the expiration timer refreshes on both read operations (get/search)
+ and write operations (put/update), whenever the item is included in the operation.
+
+ Note:
+ Indexing support depends on your store implementation.
+ If you do not initialize the store with indexing capabilities,
+ the `index` parameter will be ignored.
+
+ Similarly, TTL support depends on the specific store implementation.
+ Some implementations may not support expiration of items.
+
+ ???+ example "Examples"
+
+ Store item. Indexing depends on how you configure the store:
+
+ ```python
+ store.put(("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
+ store.put(("docs",), "report", {"memory": "Will likes ai"}, index=False)
+ ```
+
+ Index specific fields for search:
+
+ ```python
+ store.put(("docs",), "report", {"memory": "Will likes ai"}, index=["memory"])
+ ```
+ """
+ _validate_namespace(namespace)
+ if ttl not in (NOT_PROVIDED, None) and not self.supports_ttl:
+ raise NotImplementedError(
+ f"TTL is not supported by {self.__class__.__name__}. "
+ f"Use a store implementation that supports TTL or set ttl=None."
+ )
+ self.batch(
+ [
+ PutOp(
+ namespace,
+ str(key),
+ value,
+ index=index,
+ ttl=_ensure_ttl(self.ttl_config, ttl),
+ )
+ ]
+ )
+
+ def delete(self, namespace: tuple[str, ...], key: str) -> None:
+ """Delete an item.
+
+ Args:
+ namespace: Hierarchical path for the item.
+ key: Unique identifier within the namespace.
+ """
+ self.batch([PutOp(namespace, str(key), None, ttl=None)])
+
+ def list_namespaces(
+ self,
+ *,
+ prefix: NamespacePath | None = None,
+ suffix: NamespacePath | None = None,
+ max_depth: int | None = None,
+ limit: int = 100,
+ offset: int = 0,
+ ) -> list[tuple[str, ...]]:
+ """List and filter namespaces in the store.
+
+ Used to explore the organization of data,
+ find specific collections, or navigate the namespace hierarchy.
+
+ Args:
+ prefix: Filter namespaces that start with this path.
+ suffix: Filter namespaces that end with this path.
+ max_depth: Return namespaces up to this depth in the hierarchy.
+ Namespaces deeper than this level will be truncated.
+ limit: Maximum number of namespaces to return.
+ offset: Number of namespaces to skip for pagination.
+
+ Returns:
+ A list of namespace tuples that match the criteria. Each tuple represents a
+ full namespace path up to `max_depth`.
+
+ ???+ example "Examples":
+
+ Setting `max_depth=3`. Given the namespaces:
+
+ ```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:
+ 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,
+ )
+ return self.batch([op])[0]
+
+ async def aget(
+ self,
+ namespace: tuple[str, ...],
+ key: str,
+ *,
+ refresh_ttl: bool | None = None,
+ ) -> Item | None:
+ """Asynchronously retrieve a single item.
+
+ Args:
+ namespace: Hierarchical path for the item.
+ key: Unique identifier within the namespace.
+
+ Returns:
+ The retrieved item or `None` if not found.
+ """
+ return (
+ await self.abatch(
+ [
+ GetOp(
+ namespace,
+ str(key),
+ _ensure_refresh(self.ttl_config, refresh_ttl),
+ )
+ ]
+ )
+ )[0]
+
+ async def asearch(
+ self,
+ namespace_prefix: tuple[str, ...],
+ /,
+ *,
+ query: str | None = None,
+ filter: dict[str, Any] | None = None,
+ limit: int = 10,
+ offset: int = 0,
+ refresh_ttl: bool | None = None,
+ ) -> 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.
+ refresh_ttl: Whether to refresh TTLs for the returned items.
+ If `None`, uses the store's `TTLConfig.refresh_default` setting.
+ If `TTLConfig` is not provided or no TTL is specified, this argument is ignored.
+
+ 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(
+ [
+ SearchOp(
+ namespace_prefix,
+ filter,
+ limit,
+ offset,
+ query,
+ _ensure_refresh(self.ttl_config, refresh_ttl),
+ )
+ ]
+ )
+ )[0]
+
+ async def aput(
+ self,
+ namespace: tuple[str, ...],
+ key: str,
+ value: dict[str, Any],
+ index: Literal[False] | list[str] | None = None,
+ *,
+ ttl: float | None | NotProvided = NOT_PROVIDED,
+ ) -> None:
+ """Asynchronously store or update an item in the store.
+
+ Args:
+ 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 `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"`
+ - Array access: `"chapters[*].content"` (each indexed separately)
+ - Specific indices: `"authors[0].name"`
+ ttl: Time to live in minutes. Support for this argument depends on your store adapter.
+ If specified, the item will expire after this many minutes from when it was last accessed.
+ None means no expiration. Expired runs will be deleted opportunistically.
+ By default, the expiration timer refreshes on both read operations (get/search)
+ and write operations (put/update), whenever the item is included in the operation.
+
+ Note:
+ Indexing support depends on your store implementation.
+ If you do not initialize the store with indexing capabilities,
+ the `index` parameter will be ignored.
+
+ Similarly, TTL support depends on the specific store implementation.
+ Some implementations may not support expiration of items.
+
+ ???+ example "Examples"
+
+ Store item. Indexing depends on how you configure the store:
+
+ ```python
+ 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):
+
+ ```python
+ await store.aput(
+ ("docs",),
+ "report",
+ {
+ "memory": "Will likes ai",
+ "context": [{"content": "..."}, {"content": "..."}]
+ },
+ index=["memory", "context[*].content"]
+ )
+ ```
+ """
+ _validate_namespace(namespace)
+ if ttl not in (NOT_PROVIDED, None) and not self.supports_ttl:
+ raise NotImplementedError(
+ f"TTL is not supported by {self.__class__.__name__}. "
+ f"Use a store implementation that supports TTL or set ttl=None."
+ )
+ await self.abatch(
+ [
+ PutOp(
+ namespace,
+ str(key),
+ value,
+ index=index,
+ ttl=_ensure_ttl(self.ttl_config, ttl),
+ )
+ ]
+ )
+
+ async def adelete(self, namespace: tuple[str, ...], key: str) -> None:
+ """Asynchronously delete an item.
+
+ Args:
+ namespace: Hierarchical path for the item.
+ key: Unique identifier within the namespace.
+ """
+ await self.abatch([PutOp(namespace, str(key), None)])
+
+ async def alist_namespaces(
+ self,
+ *,
+ prefix: NamespacePath | None = None,
+ suffix: NamespacePath | None = None,
+ max_depth: int | None = None,
+ limit: int = 100,
+ offset: int = 0,
+ ) -> list[tuple[str, ...]]:
+ """List and filter namespaces in the store asynchronously.
+
+ Used to explore the organization of data,
+ find specific collections, or navigate the namespace hierarchy.
+
+ Args:
+ prefix: Filter namespaces that start with this path.
+ suffix: Filter namespaces that end with this path.
+ max_depth: Return namespaces up to this depth in the hierarchy.
+ Namespaces deeper than this level will be truncated to this depth.
+ limit: Maximum number of namespaces to return.
+ offset: Number of namespaces to skip for pagination.
+
+ Returns:
+ A list of namespace tuples that match the criteria. Each tuple represents a
+ full namespace path up to `max_depth`.
+
+ ???+ 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")
+
+ await store.alist_namespaces(prefix=("a", "b"), max_depth=3)
+ # Returns: [("a", "b", "c"), ("a", "b", "d"), ("a", "b", "f")]
+ ```
+ """
+ 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,
+ )
+ 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}'
+ )
+
+
+def _ensure_refresh(
+ ttl_config: TTLConfig | None, refresh_ttl: bool | None = None
+) -> bool:
+ if refresh_ttl is not None:
+ return refresh_ttl
+ if ttl_config is not None:
+ return ttl_config.get("refresh_on_read", True)
+ return True
+
+
+def _ensure_ttl(
+ ttl_config: TTLConfig | None,
+ ttl: float | None | NotProvided = NOT_PROVIDED,
+) -> float | None:
+ if ttl is NOT_PROVIDED:
+ if ttl_config:
+ return ttl_config.get("default_ttl")
+ return None
+ return ttl
+
+
+__all__ = [
+ "BaseStore",
+ "Item",
+ "Op",
+ "PutOp",
+ "GetOp",
+ "SearchOp",
+ "ListNamespacesOp",
+ "MatchCondition",
+ "NamespacePath",
+ "NamespaceMatchType",
+ "Embeddings",
+ "ensure_embeddings",
+ "tokenize_path",
+ "get_text_at_path",
+]
diff --git a/langgraph/source/libs/checkpoint/langgraph/store/base/batch.py b/langgraph/source/libs/checkpoint/langgraph/store/base/batch.py
new file mode 100644
index 0000000000000000000000000000000000000000..0a4a0eef74d2454eca4c76bc6a8196d101116a6a
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/store/base/batch.py
@@ -0,0 +1,365 @@
+"""Utilities for batching operations in a background task."""
+
+from __future__ import annotations
+
+import asyncio
+import functools
+import weakref
+from collections.abc import Callable, Iterable
+from typing import Any, Literal, TypeVar
+
+from langgraph.store.base import (
+ NOT_PROVIDED,
+ BaseStore,
+ GetOp,
+ Item,
+ ListNamespacesOp,
+ MatchCondition,
+ NamespacePath,
+ NotProvided,
+ Op,
+ PutOp,
+ Result,
+ SearchItem,
+ SearchOp,
+ _ensure_refresh,
+ _ensure_ttl,
+ _validate_namespace,
+)
+
+F = TypeVar("F", bound=Callable)
+
+
+def _check_loop(func: F) -> F:
+ @functools.wraps(func)
+ def wrapper(store: AsyncBatchedBaseStore, *args: Any, **kwargs: Any) -> Any:
+ method_name: str = func.__name__
+ try:
+ current_loop = asyncio.get_running_loop()
+ if current_loop is store._loop:
+ replacement_str = (
+ f"Specifically, replace `store.{method_name}(...)` with `await store.a{method_name}(...)"
+ if method_name
+ else "For example, replace `store.get(...)` with `await store.aget(...)`"
+ )
+ raise asyncio.InvalidStateError(
+ f"Synchronous calls to {store.__class__.__name__} detected in the main event loop. "
+ "This can lead to deadlocks or performance issues. "
+ "Please use the asynchronous interface for main thread operations. "
+ f"{replacement_str} "
+ )
+ except RuntimeError:
+ pass
+ return func(store, *args, **kwargs)
+
+ return wrapper
+
+
+class AsyncBatchedBaseStore(BaseStore):
+ """Efficiently batch operations in a background task."""
+
+ __slots__ = ("_loop", "_aqueue", "_task")
+
+ def __init__(self) -> None:
+ super().__init__()
+ self._loop = asyncio.get_running_loop()
+ self._aqueue: asyncio.Queue[tuple[asyncio.Future, Op]] = asyncio.Queue()
+ self._task: asyncio.Task | None = None
+ self._ensure_task()
+
+ def __del__(self) -> None:
+ try:
+ if self._task:
+ self._task.cancel()
+ except RuntimeError:
+ pass
+
+ def _ensure_task(self) -> None:
+ """Ensure the background processing loop is running."""
+ if self._task is None or self._task.done():
+ self._task = self._loop.create_task(_run(self._aqueue, weakref.ref(self)))
+
+ async def aget(
+ self,
+ namespace: tuple[str, ...],
+ key: str,
+ *,
+ refresh_ttl: bool | None = None,
+ ) -> Item | None:
+ self._ensure_task()
+ fut = self._loop.create_future()
+ self._aqueue.put_nowait(
+ (
+ fut,
+ GetOp(
+ namespace,
+ key,
+ refresh_ttl=_ensure_refresh(self.ttl_config, refresh_ttl),
+ ),
+ )
+ )
+ return await fut
+
+ async def asearch(
+ self,
+ namespace_prefix: tuple[str, ...],
+ /,
+ *,
+ query: str | None = None,
+ filter: dict[str, Any] | None = None,
+ limit: int = 10,
+ offset: int = 0,
+ refresh_ttl: bool | None = None,
+ ) -> list[SearchItem]:
+ self._ensure_task()
+ fut = self._loop.create_future()
+ self._aqueue.put_nowait(
+ (
+ fut,
+ SearchOp(
+ namespace_prefix,
+ filter,
+ limit,
+ offset,
+ query,
+ refresh_ttl=_ensure_refresh(self.ttl_config, refresh_ttl),
+ ),
+ )
+ )
+ return await fut
+
+ async def aput(
+ self,
+ namespace: tuple[str, ...],
+ key: str,
+ value: dict[str, Any],
+ index: Literal[False] | list[str] | None = None,
+ *,
+ ttl: float | None | NotProvided = NOT_PROVIDED,
+ ) -> None:
+ self._ensure_task()
+ _validate_namespace(namespace)
+ fut = self._loop.create_future()
+ self._aqueue.put_nowait(
+ (
+ fut,
+ PutOp(
+ namespace, key, value, index, ttl=_ensure_ttl(self.ttl_config, ttl)
+ ),
+ )
+ )
+ return await fut
+
+ async def adelete(
+ self,
+ namespace: tuple[str, ...],
+ key: str,
+ ) -> None:
+ self._ensure_task()
+ fut = self._loop.create_future()
+ self._aqueue.put_nowait((fut, PutOp(namespace, key, None)))
+ return await fut
+
+ async def alist_namespaces(
+ self,
+ *,
+ prefix: NamespacePath | None = None,
+ suffix: NamespacePath | None = None,
+ max_depth: int | None = None,
+ limit: int = 100,
+ offset: int = 0,
+ ) -> list[tuple[str, ...]]:
+ self._ensure_task()
+ 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.put_nowait((fut, op))
+ return await fut
+
+ @_check_loop
+ def batch(self, ops: Iterable[Op]) -> list[Result]:
+ return asyncio.run_coroutine_threadsafe(self.abatch(ops), self._loop).result()
+
+ @_check_loop
+ def get(
+ self,
+ namespace: tuple[str, ...],
+ key: str,
+ *,
+ refresh_ttl: bool | None = None,
+ ) -> Item | None:
+ return asyncio.run_coroutine_threadsafe(
+ self.aget(namespace, key=key, refresh_ttl=refresh_ttl), self._loop
+ ).result()
+
+ @_check_loop
+ def search(
+ self,
+ namespace_prefix: tuple[str, ...],
+ /,
+ *,
+ query: str | None = None,
+ filter: dict[str, Any] | None = None,
+ limit: int = 10,
+ offset: int = 0,
+ refresh_ttl: bool | None = None,
+ ) -> list[SearchItem]:
+ return asyncio.run_coroutine_threadsafe(
+ self.asearch(
+ namespace_prefix,
+ query=query,
+ filter=filter,
+ limit=limit,
+ offset=offset,
+ refresh_ttl=refresh_ttl,
+ ),
+ self._loop,
+ ).result()
+
+ @_check_loop
+ def put(
+ self,
+ namespace: tuple[str, ...],
+ key: str,
+ value: dict[str, Any],
+ index: Literal[False] | list[str] | None = None,
+ *,
+ ttl: float | None | NotProvided = NOT_PROVIDED,
+ ) -> None:
+ _validate_namespace(namespace)
+ asyncio.run_coroutine_threadsafe(
+ self.aput(
+ namespace,
+ key=key,
+ value=value,
+ index=index,
+ ttl=_ensure_ttl(self.ttl_config, ttl),
+ ),
+ self._loop,
+ ).result()
+
+ @_check_loop
+ def delete(
+ self,
+ namespace: tuple[str, ...],
+ key: str,
+ ) -> None:
+ asyncio.run_coroutine_threadsafe(
+ self.adelete(namespace, key=key), self._loop
+ ).result()
+
+ @_check_loop
+ def list_namespaces(
+ self,
+ *,
+ prefix: NamespacePath | None = None,
+ suffix: NamespacePath | None = None,
+ max_depth: int | None = None,
+ limit: int = 100,
+ offset: int = 0,
+ ) -> list[tuple[str, ...]]:
+ return asyncio.run_coroutine_threadsafe(
+ self.alist_namespaces(
+ prefix=prefix,
+ suffix=suffix,
+ max_depth=max_depth,
+ limit=limit,
+ offset=offset,
+ ),
+ self._loop,
+ ).result()
+
+
+def _dedupe_ops(values: list[Op]) -> tuple[list[int] | None, 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: asyncio.Queue[tuple[asyncio.Future, Op]],
+ store: weakref.ReferenceType[BaseStore],
+) -> None:
+ while item := await aqueue.get():
+ # check if store is still alive
+ if s := store():
+ try:
+ # accumulate operations scheduled in same tick
+ items = [item]
+ try:
+ while item := aqueue.get_nowait():
+ items.append(item)
+ except asyncio.QueueEmpty:
+ pass
+ # get the operations to run
+ futs = [item[0] for item in items]
+ values = [item[1] for item in items]
+ # action each operation
+ try:
+ 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(futs, results, strict=False):
+ # guard against future being done (e.g. cancelled)
+ if not fut.done():
+ fut.set_result(result)
+ except Exception as e:
+ for fut in futs:
+ # guard against future being done (e.g. cancelled)
+ if not fut.done():
+ fut.set_exception(e)
+ finally:
+ # remove strong ref to store
+ del s
+ else:
+ break
diff --git a/langgraph/source/libs/checkpoint/langgraph/store/base/embed.py b/langgraph/source/libs/checkpoint/langgraph/store/base/embed.py
new file mode 100644
index 0000000000000000000000000000000000000000..4255886e2b30b511d41e35f6df420f5f6de18a70
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/store/base/embed.py
@@ -0,0 +1,433 @@
+"""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.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import functools
+import json
+from collections.abc import Awaitable, Callable, Sequence
+from typing import Any
+
+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: Embeddings | EmbeddingsFunc | AEmbeddingsFunc | str | 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]
+ ```
+
+ Initialize embeddings using a provider string:
+
+ ```python
+ # Requires langchain>=0.3.9 and langgraph-checkpoint>=2.0.11
+ embeddings = ensure_embeddings("openai:text-embedding-3-small")
+ result = embeddings.embed_query("hello")
+ ```
+ """
+ if embed is None:
+ raise ValueError("embed must be provided")
+ if isinstance(embed, str):
+ init_embeddings = _get_init_embeddings()
+ if init_embeddings is None:
+ from importlib.metadata import PackageNotFoundError, version
+
+ try:
+ lc_version = version("langchain")
+ version_info = f"Found langchain version {lc_version}, but"
+ except PackageNotFoundError:
+ version_info = "langchain is not installed;"
+
+ raise ValueError(
+ f"Could not load embeddings from string '{embed}'. {version_info} "
+ "loading embeddings by provider:identifier string requires langchain>=0.3.9 "
+ "as well as the provider-specific package. "
+ "Install LangChain with: pip install 'langchain>=0.3.9' "
+ "and the provider-specific package (e.g., 'langchain-openai>=0.3.0'). "
+ "Alternatively, specify 'embed' as a compatible Embeddings object or python function."
+ )
+ return init_embeddings(embed)
+
+ 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: 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: 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, ensure_ascii=False)]
+
+ 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, ensure_ascii=False)]
+ 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: dict | None = 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, ensure_ascii=False
+ )
+ )
+
+ # 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__)
+ )
+
+
+@functools.lru_cache
+def _get_init_embeddings() -> Callable[[str], Embeddings] | None:
+ try:
+ from langchain.embeddings import init_embeddings # type: ignore
+
+ return init_embeddings
+ except ImportError:
+ return None
+
+
+__all__ = [
+ "ensure_embeddings",
+ "EmbeddingsFunc",
+ "AEmbeddingsFunc",
+]
diff --git a/langgraph/source/libs/checkpoint/langgraph/store/base/py.typed b/langgraph/source/libs/checkpoint/langgraph/store/base/py.typed
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/checkpoint/langgraph/store/memory/__init__.py b/langgraph/source/libs/checkpoint/langgraph/store/memory/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..b156c457df67ca021e090d90c1373e2d713a2123
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/langgraph/store/memory/__init__.py
@@ -0,0 +1,592 @@
+"""In-memory dictionary-backed store with optional vector search.
+
+!!! example "Examples"
+ Basic key-value storage:
+ ```python
+ from langgraph.store.memory import InMemoryStore
+
+ 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": init_embeddings("openai: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")
+ ```
+
+ Vector search using OpenAI SDK directly:
+ ```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
+ }
+ )
+
+ # 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
+ ```
+"""
+
+from __future__ import annotations
+
+import asyncio
+import concurrent.futures as cf
+import functools
+import logging
+from collections import defaultdict
+from collections.abc import Iterable
+from datetime import datetime, timezone
+from importlib import util
+from typing import Any
+
+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):
+ """In-memory dictionary-backed store with optional vector search.
+
+ !!! 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.embeddings import init_embeddings
+ store = InMemoryStore(index={
+ "dims": 1536,
+ "embed": init_embeddings("openai:text-embedding-3-small"),
+ "fields": ["text"],
+ })
+
+ # 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:
+ 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.
+
+ Tip:
+ For vector search, install numpy for better performance:
+ ```bash
+ pip install numpy
+ ```
+ """
+
+ __slots__ = (
+ "_data",
+ "_vectors",
+ "index_config",
+ "embeddings",
+ )
+
+ def __init__(self, *, index: IndexConfig | None = 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: Embeddings | None = 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, strict=False))
+
+ 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, strict=False),
+ key=lambda x: x[0],
+ reverse=True,
+ )
+ # max pooling
+ seen: set[tuple[tuple[str, ...], str]] = set()
+ kept: list[tuple[float | None, 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] = []
+ 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):
+ search_ops[i] = (op, self._filter_items(op))
+ results.append(None)
+ elif isinstance(op, ListNamespacesOp):
+ results.append(self._handle_list_namespaces(op))
+ elif isinstance(op, PutOp):
+ put_ops[(op.namespace, op.key)] = op
+ results.append(None)
+ else:
+ raise ValueError(f"Unknown operation type: {type(op)}")
+
+ 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, strict=False):
+ self._vectors[ns][key][path] = embedding
+
+ def _handle_list_namespaces(self, op: ListNamespacesOp) -> list[tuple[str, ...]]:
+ all_namespaces = list(
+ self._data.keys()
+ ) # Avoid collection size changing while iterating
+ namespaces = all_namespaces
+ if op.match_conditions:
+ namespaces = [
+ ns
+ for ns in namespaces
+ if all(_does_match(condition, ns) for condition in op.match_conditions)
+ ]
+
+ if op.max_depth is not None:
+ namespaces = sorted({ns[: op.max_depth] for ns in namespaces})
+ else:
+ namespaces = sorted(namespaces)
+ 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 not Y:
+ return []
+ if _check_numpy():
+ import numpy as np
+
+ 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, strict=False))
+ 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
+
+ if len(key) < len(path):
+ return False
+
+ if match_type == "prefix":
+ for k_elem, p_elem in zip(key, path, strict=False):
+ if p_elem == "*":
+ continue # Wildcard matches any element
+ if k_elem != p_elem:
+ return False
+ return True
+ elif match_type == "suffix":
+ for k_elem, p_elem in zip(reversed(key), reversed(path), strict=False):
+ if p_elem == "*":
+ continue # Wildcard matches any element
+ if k_elem != p_elem:
+ return False
+ 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, strict=False)
+ )
+ )
+ 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/langgraph/source/libs/checkpoint/langgraph/store/memory/py.typed b/langgraph/source/libs/checkpoint/langgraph/store/memory/py.typed
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/checkpoint/pyproject.toml b/langgraph/source/libs/checkpoint/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..71d104de3341936f438d1f9daa32efd34f56f594
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/pyproject.toml
@@ -0,0 +1,80 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "langgraph-checkpoint"
+version = "4.0.0"
+description = "Library with base interfaces for LangGraph checkpoint savers."
+authors = []
+requires-python = ">=3.10"
+readme = "README.md"
+license = "MIT"
+license-files = ['LICENSE']
+dependencies = [
+ "langchain-core>=0.2.38",
+ "ormsgpack>=1.12.0",
+]
+
+[project.urls]
+Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint"
+Twitter = "https://x.com/LangChain"
+Slack = "https://www.langchain.com/join-community"
+Reddit = "https://www.reddit.com/r/LangChain/"
+
+[dependency-groups]
+test = [
+ "pytest",
+ "pytest-asyncio",
+ "pytest-mock",
+ "pytest-watcher",
+ "dataclasses-json",
+ "numpy",
+ "pandas",
+ "pandas-stubs>=2.2.2.240807",
+ "redis",
+]
+lint = [
+ "ruff",
+ "codespell",
+ "mypy",
+]
+dev = [
+ {include-group = "test"},
+ {include-group = "lint"},
+]
+
+[tool.hatch.build.targets.wheel]
+include = ["langgraph"]
+
+[tool.pytest.ini_options]
+addopts = "--strict-markers --strict-config --durations=5 -vv"
+asyncio_mode = "auto"
+
+[tool.ruff]
+lint.select = [
+ "E", # pycodestyle
+ "F", # Pyflakes
+ "UP", # pyupgrade
+ "B", # flake8-bugbear
+ "I", # isort
+ "UP", # pyupgrade
+]
+lint.ignore = ["E501", "B008"]
+target-version = "py310"
+
+[tool.pytest-watcher]
+now = true
+delay = 0.1
+runner_args = ["--ff", "-v", "--tb", "short"]
+patterns = ["*.py"]
+
+[tool.mypy]
+# https://mypy.readthedocs.io/en/stable/config_file.html
+disallow_untyped_defs = "True"
+explicit_package_bases = "True"
+warn_no_return = "False"
+warn_unused_ignores = "True"
+warn_redundant_casts = "True"
+allow_redefinition = "True"
+disable_error_code = "typeddict-item, return-value"
diff --git a/langgraph/source/libs/checkpoint/tests/__init__.py b/langgraph/source/libs/checkpoint/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/checkpoint/tests/embed_test_utils.py b/langgraph/source/libs/checkpoint/tests/embed_test_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..d28cd959f380264e1e651b7782e65c354f33ae4c
--- /dev/null
+++ b/langgraph/source/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/langgraph/source/libs/checkpoint/tests/test_jsonplus.py b/langgraph/source/libs/checkpoint/tests/test_jsonplus.py
new file mode 100644
index 0000000000000000000000000000000000000000..c2ff5f2d0a27042eb2a53940043e409cda51724e
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/tests/test_jsonplus.py
@@ -0,0 +1,516 @@
+import dataclasses
+import json
+import pathlib
+import re
+import sys
+import uuid
+from collections import deque
+from datetime import date, datetime, time, timezone
+from decimal import Decimal
+from enum import Enum
+from ipaddress import IPv4Address
+from zoneinfo import ZoneInfo
+
+import dataclasses_json
+import numpy as np
+import pandas as pd
+import pytest
+from pydantic import BaseModel, SecretStr
+from pydantic.v1 import BaseModel as BaseModelV1
+from pydantic.v1 import SecretStr as SecretStrV1
+
+from langgraph.checkpoint.serde.jsonplus import (
+ InvalidModuleError,
+ JsonPlusSerializer,
+ _msgpack_ext_hook_to_json,
+)
+from langgraph.store.base import Item
+
+
+class InnerPydantic(BaseModel):
+ hello: str
+
+
+class MyPydantic(BaseModel):
+ foo: str
+ bar: int
+ inner: InnerPydantic
+
+
+class InnerPydanticV1(BaseModelV1):
+ hello: str
+
+
+class MyPydanticV1(BaseModelV1):
+ foo: str
+ bar: int
+ inner: InnerPydanticV1
+
+
+@dataclasses.dataclass
+class InnerDataclass:
+ hello: str
+
+
+@dataclasses.dataclass
+class MyDataclass:
+ foo: str
+ bar: int
+ inner: InnerDataclass
+
+ def something(self) -> None:
+ pass
+
+
+@dataclasses.dataclass(slots=True)
+class MyDataclassWSlots:
+ foo: str
+ bar: int
+ inner: InnerDataclass
+
+ def something(self) -> None:
+ pass
+
+
+class MyEnum(Enum):
+ FOO = "foo"
+ BAR = "bar"
+
+
+@dataclasses_json.dataclass_json
+@dataclasses.dataclass
+class Person:
+ name: str
+
+
+def test_serde_jsonplus() -> None:
+ uid = uuid.UUID(int=1)
+ deque_instance = deque([1, 2, 3])
+ tzn = ZoneInfo("America/New_York")
+ ip4 = IPv4Address("192.168.0.1")
+ current_date = date(2024, 4, 19)
+ current_time = time(23, 4, 57, 51022, timezone.max)
+ current_timestamp = datetime(2024, 4, 19, 23, 4, 57, 51022, timezone.max)
+
+ to_serialize = {
+ "path": pathlib.Path("foo", "bar"),
+ "re": re.compile(r"foo", re.DOTALL),
+ "decimal": Decimal("1.10101"),
+ "set": {1, 2, frozenset({1, 2})},
+ "frozen_set": frozenset({1, 2, 3}),
+ "ip4": ip4,
+ "deque": deque_instance,
+ "tzn": tzn,
+ "date": current_date,
+ "time": current_time,
+ "uid": uid,
+ "timestamp": current_timestamp,
+ "my_rich_dict": {(1, 2, 3): 45},
+ "my_slotted_class": MyDataclassWSlots("bar", 2, InnerDataclass("hello")),
+ "my_dataclass": MyDataclass("foo", 1, InnerDataclass("hello")),
+ "my_enum": MyEnum.FOO,
+ "my_pydantic": MyPydantic(foo="foo", bar=1, inner=InnerPydantic(hello="hello")),
+ "my_secret_str": SecretStr("meow"),
+ "person": Person(name="foo"),
+ "a_bool": True,
+ "a_none": None,
+ "a_str": "foo",
+ "a_str_nuc": "foo\u0000",
+ "a_str_uc": "foo ⛰️",
+ "a_str_ucuc": "foo \u26f0\ufe0f\u0000",
+ "a_str_ucucuc": "foo \\u26f0\\ufe0f",
+ "an_int": 1,
+ "a_float": 1.1,
+ "a_bytes": b"my bytes",
+ "a_bytearray": bytearray([42]),
+ "my_item": Item(
+ value={},
+ key="my-key",
+ namespace=("a", "name", " "),
+ created_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
+ updated_at=datetime(2024, 9, 24, 17, 29, 11, 128397),
+ ),
+ }
+
+ if sys.version_info < (3, 14):
+ to_serialize["my_pydantic_v1"] = MyPydanticV1(
+ foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
+ )
+ to_serialize["my_secret_str_v1"] = SecretStrV1("meow")
+
+ serde = JsonPlusSerializer()
+
+ dumped = serde.dumps_typed(to_serialize)
+
+ assert dumped[0] == "msgpack"
+ assert serde.loads_typed(dumped) == to_serialize
+
+ for value in to_serialize.values():
+ assert serde.loads_typed(serde.dumps_typed(value)) == value
+
+ surrogates = [
+ "Hello??",
+ "Python??",
+ "Surrogate??",
+ "Example??",
+ "String??",
+ "With??",
+ "Surrogates??",
+ "Embedded??",
+ "In??",
+ "The??",
+ "Text??",
+ "收花🙄·到",
+ ]
+ serde = JsonPlusSerializer(pickle_fallback=False)
+
+ assert serde.loads_typed(serde.dumps_typed(surrogates)) == surrogates
+
+
+def test_serde_jsonplus_json_mode() -> None:
+ uid = uuid.UUID(int=1)
+ deque_instance = deque([1, 2, 3])
+ tzn = ZoneInfo("America/New_York")
+ ip4 = IPv4Address("192.168.0.1")
+ current_date = date(2024, 4, 19)
+ current_time = time(23, 4, 57, 51022, timezone.max)
+ current_timestamp = datetime(2024, 4, 19, 23, 4, 57, 51022, timezone.max)
+
+ to_serialize = {
+ "path": pathlib.Path("foo", "bar"),
+ "re": re.compile(r"foo", re.DOTALL),
+ "decimal": Decimal("1.10101"),
+ "set": {1, 2, frozenset({1, 2})},
+ "frozen_set": frozenset({1, 2, 3}),
+ "ip4": ip4,
+ "deque": deque_instance,
+ "tzn": tzn,
+ "date": current_date,
+ "time": current_time,
+ "uid": uid,
+ "timestamp": current_timestamp,
+ "my_slotted_class": MyDataclassWSlots("bar", 2, InnerDataclass("hello")),
+ "my_dataclass": MyDataclass("foo", 1, InnerDataclass("hello")),
+ "my_enum": MyEnum.FOO,
+ "my_pydantic": MyPydantic(foo="foo", bar=1, inner=InnerPydantic(hello="hello")),
+ "my_secret_str": SecretStr("meow"),
+ "person": Person(name="foo"),
+ "a_bool": True,
+ "a_none": None,
+ "a_str": "foo",
+ "a_str_nuc": "foo\u0000",
+ "a_str_uc": "foo ⛰️",
+ "a_str_ucuc": "foo \u26f0\ufe0f\u0000",
+ "a_str_ucucuc": "foo \\u26f0\\ufe0f",
+ "an_int": 1,
+ "a_float": 1.1,
+ "a_bytes": b"my bytes",
+ "a_bytearray": bytearray([42]),
+ "my_item": Item(
+ value={},
+ key="my-key",
+ namespace=("a", "name", " "),
+ created_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
+ updated_at=datetime(2024, 9, 24, 17, 29, 11, 128397),
+ ),
+ }
+
+ if sys.version_info < (3, 14):
+ to_serialize["my_pydantic_v1"] = MyPydanticV1(
+ foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
+ )
+ to_serialize["my_secret_str_v1"] = SecretStrV1("meow")
+
+ serde = JsonPlusSerializer(__unpack_ext_hook__=_msgpack_ext_hook_to_json)
+
+ dumped = serde.dumps_typed(to_serialize)
+
+ assert dumped[0] == "msgpack"
+ result = serde.loads_typed(dumped)
+
+ expected_result = {
+ "path": ["foo", "bar"],
+ "re": ["foo", 48],
+ "decimal": "1.10101",
+ "set": [1, 2, [1, 2]],
+ "frozen_set": [1, 2, 3],
+ "ip4": "192.168.0.1",
+ "deque": [1, 2, 3],
+ "tzn": "America/New_York",
+ "date": [2024, 4, 19],
+ "time": {
+ "hour": 23,
+ "minute": 4,
+ "second": 57,
+ "microsecond": 51022,
+ "tzinfo": [[0, 86340, 0]],
+ "fold": 0,
+ },
+ "uid": "00000000-0000-0000-0000-000000000001",
+ "timestamp": "2024-04-19T23:04:57.051022+23:59",
+ "my_slotted_class": {"foo": "bar", "bar": 2, "inner": {"hello": "hello"}},
+ "my_dataclass": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
+ "my_enum": "foo",
+ "my_pydantic": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
+ "my_secret_str": "meow",
+ "person": {"name": "foo"},
+ "a_bool": True,
+ "a_none": None,
+ "a_str": "foo",
+ "a_str_nuc": "foo\x00",
+ "a_str_uc": "foo ⛰️",
+ "a_str_ucuc": "foo ⛰️\x00",
+ "a_str_ucucuc": "foo \\u26f0\\ufe0f",
+ "an_int": 1,
+ "a_float": 1.1,
+ "a_bytes": b"my bytes",
+ "a_bytearray": b"*",
+ "my_item": {
+ "namespace": ["a", "name", " "],
+ "key": "my-key",
+ "value": {},
+ "created_at": "2024-09-24T17:29:10.128397",
+ "updated_at": "2024-09-24T17:29:11.128397",
+ },
+ }
+
+ if sys.version_info < (3, 14):
+ expected_result["my_pydantic_v1"] = {
+ "foo": "foo",
+ "bar": 1,
+ "inner": {"hello": "hello"},
+ }
+ expected_result["my_secret_str_v1"] = "meow"
+
+ assert result == expected_result
+
+
+def test_serde_jsonplus_bytes() -> None:
+ serde = JsonPlusSerializer()
+
+ some_bytes = b"my bytes"
+ dumped = serde.dumps_typed(some_bytes)
+
+ assert dumped == ("bytes", some_bytes)
+ assert serde.loads_typed(dumped) == some_bytes
+
+
+def test_deserde_invalid_module() -> None:
+ serde = JsonPlusSerializer()
+ load = {
+ "lc": 2,
+ "type": "constructor",
+ "id": ["pprint", "pprint"],
+ "kwargs": {"object": "HELLO"},
+ }
+ with pytest.raises(InvalidModuleError):
+ serde._revive_lc2(load)
+ serde = JsonPlusSerializer(allowed_json_modules=[("pprint", "pprint")])
+ serde.loads_typed(("json", json.dumps(load).encode("utf-8")))
+
+
+def test_serde_jsonplus_bytearray() -> None:
+ serde = JsonPlusSerializer()
+
+ some_bytearray = bytearray([42])
+ dumped = serde.dumps_typed(some_bytearray)
+
+ assert dumped == ("bytearray", some_bytearray)
+ assert serde.loads_typed(dumped) == some_bytearray
+
+
+@pytest.mark.parametrize(
+ "arr",
+ [
+ np.arange(9, dtype=np.int32).reshape(3, 3),
+ np.asfortranarray(np.arange(9, dtype=np.float64).reshape(3, 3)),
+ np.arange(12, dtype=np.int16)[::2].reshape(3, 2),
+ ],
+)
+def test_serde_jsonplus_numpy_array(arr: np.ndarray) -> None:
+ serde = JsonPlusSerializer()
+
+ dumped = serde.dumps_typed(arr)
+ assert dumped[0] == "msgpack"
+ result = serde.loads_typed(dumped)
+ assert isinstance(result, np.ndarray)
+ assert result.dtype == arr.dtype
+ assert np.array_equal(result, arr)
+
+
+@pytest.mark.parametrize(
+ "arr",
+ [
+ np.arange(6, dtype=np.float32).reshape(2, 3),
+ np.asfortranarray(np.arange(4, dtype=np.complex128).reshape(2, 2)),
+ ],
+)
+def test_serde_jsonplus_numpy_array_json_hook(arr: np.ndarray) -> None:
+ serde = JsonPlusSerializer(__unpack_ext_hook__=_msgpack_ext_hook_to_json)
+ dumped = serde.dumps_typed(arr)
+ assert dumped[0] == "msgpack"
+ result = serde.loads_typed(dumped)
+ assert isinstance(result, list)
+ assert result == arr.tolist()
+
+
+@pytest.mark.parametrize(
+ "df",
+ [
+ pd.DataFrame(),
+ pd.DataFrame({"int_col": [1, 2, 3]}),
+ pd.DataFrame({"float_col": [1.1, 2.2, 3.3]}),
+ pd.DataFrame({"str_col": ["a", "b", "c"]}),
+ pd.DataFrame({"bool_col": [True, False, True]}),
+ pd.DataFrame(
+ {
+ "datetime_col": [
+ datetime(2024, 1, 1),
+ datetime(2024, 1, 2),
+ datetime(2024, 1, 3),
+ ]
+ }
+ ),
+ pd.DataFrame(
+ {
+ "int_col": [1, 2, 3],
+ "float_col": [1.1, 2.2, 3.3],
+ "str_col": ["a", "b", "c"],
+ }
+ ),
+ pd.DataFrame(
+ {
+ "int_col": [1, 2, None],
+ "float_col": [1.1, None, 3.3],
+ "str_col": ["a", None, "c"],
+ }
+ ),
+ pytest.param(
+ pd.DataFrame({"cat_col": pd.Categorical(["a", "b", "a", "c"])}),
+ marks=pytest.mark.skipif(
+ sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
+ ),
+ ),
+ pd.DataFrame(
+ {
+ "int8": pd.array([1, 2, 3], dtype="int8"),
+ "int16": pd.array([10, 20, 30], dtype="int16"),
+ "int32": pd.array([100, 200, 300], dtype="int32"),
+ "int64": pd.array([1000, 2000, 3000], dtype="int64"),
+ "float32": pd.array([1.1, 2.2, 3.3], dtype="float32"),
+ "float64": pd.array([10.1, 20.2, 30.3], dtype="float64"),
+ }
+ ),
+ pd.DataFrame({"value": [1, 2, 3]}, index=["x", "y", "z"]),
+ pd.DataFrame(
+ [[1, 2, 3, 4]],
+ columns=pd.MultiIndex.from_tuples(
+ [("A", "X"), ("A", "Y"), ("B", "X"), ("B", "Y")]
+ ),
+ ),
+ pd.DataFrame(
+ {"value": [1, 2, 3]}, index=pd.date_range("2024-01-01", periods=3, freq="D")
+ ),
+ pd.DataFrame(
+ {
+ "col1": range(1000),
+ "col2": [f"str_{i}" for i in range(1000)],
+ "col3": np.random.rand(1000),
+ }
+ ),
+ pytest.param(
+ pd.DataFrame(
+ {
+ "tz_datetime": pd.date_range(
+ "2024-01-01", periods=3, freq="D", tz="UTC"
+ )
+ }
+ ),
+ marks=pytest.mark.skipif(
+ sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
+ ),
+ ),
+ pd.DataFrame({"timedelta": pd.to_timedelta([1, 2, 3], unit="D")}),
+ pytest.param(
+ pd.DataFrame({"period": pd.period_range("2024-01", periods=3, freq="M")}),
+ marks=pytest.mark.skipif(
+ sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
+ ),
+ ),
+ pd.DataFrame({"interval": pd.interval_range(start=0, end=3, periods=3)}),
+ pd.DataFrame({"unicode": ["Hello 🌍", "Python 🐍", "Data 📊"]}),
+ pd.DataFrame({"mixed": [1, "string", [1, 2, 3], {"key": "value"}]}),
+ pd.DataFrame({"a": [1], "b": ["test"], "c": [3.14]}),
+ pd.DataFrame({"single": [42]}),
+ pd.DataFrame(
+ {
+ "small": [sys.float_info.min, 0, sys.float_info.max],
+ "large_int": [-(2**63), 0, 2**63 - 1],
+ }
+ ),
+ pd.DataFrame({"special_strings": ["", "null", "None", "NaN", "inf", "-inf"]}),
+ pd.DataFrame({"bytes_col": [b"hello", b"world", b"\x00\x01\x02"]}),
+ ],
+)
+def test_serde_jsonplus_pandas_dataframe(df: pd.DataFrame) -> None:
+ serde = JsonPlusSerializer(pickle_fallback=True)
+
+ dumped = serde.dumps_typed(df)
+ assert dumped[0] == "pickle"
+ result = serde.loads_typed(dumped)
+ assert result.equals(df)
+
+
+@pytest.mark.parametrize(
+ "series",
+ [
+ pd.Series([]),
+ pd.Series([1, 2, 3]),
+ pd.Series([1.1, 2.2, 3.3]),
+ pd.Series(["a", "b", "c"]),
+ pd.Series([True, False, True]),
+ pd.Series([datetime(2024, 1, 1), datetime(2024, 1, 2), datetime(2024, 1, 3)]),
+ pd.Series([1, 2, None]),
+ pd.Series([1.1, None, 3.3]),
+ pd.Series(["a", None, "c"]),
+ pytest.param(
+ pd.Series(pd.Categorical(["a", "b", "a", "c"])),
+ marks=pytest.mark.skipif(
+ sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
+ ),
+ ),
+ pd.Series([1, 2, 3], dtype="int8"),
+ pd.Series([10, 20, 30], dtype="int16"),
+ pd.Series([100, 200, 300], dtype="int32"),
+ pd.Series([1000, 2000, 3000], dtype="int64"),
+ pd.Series([1.1, 2.2, 3.3], dtype="float32"),
+ pd.Series([10.1, 20.2, 30.3], dtype="float64"),
+ pd.Series([1, 2, 3], index=["x", "y", "z"]),
+ pd.Series([1, 2, 3], index=pd.date_range("2024-01-01", periods=3, freq="D")),
+ pd.Series(range(1000)),
+ pd.Series(pd.date_range("2024-01-01", periods=3, freq="D", tz="UTC")),
+ pd.Series(pd.to_timedelta([1, 2, 3], unit="D")),
+ pd.Series(pd.period_range("2024-01", periods=3, freq="M")),
+ pd.Series(pd.interval_range(start=0, end=3, periods=3)),
+ pd.Series(["Hello 🌍", "Python 🐍", "Data 📊"]),
+ pd.Series([1, "string", [1, 2, 3], {"key": "value"}]),
+ pd.Series([42], name="single"),
+ pd.Series([sys.float_info.min, 0, sys.float_info.max]),
+ pd.Series([-(2**63), 0, 2**63 - 1]),
+ pd.Series(["", "null", "None", "NaN", "inf", "-inf"]),
+ pd.Series([b"hello", b"world", b"\x00\x01\x02"]),
+ pd.Series([1, 2, 3], name="named_series"),
+ pd.Series(
+ [10, 20],
+ index=pd.MultiIndex.from_tuples([("a", 1), ("b", 2)], names=["x", "y"]),
+ ),
+ ],
+)
+def test_serde_jsonplus_pandas_series(series: pd.Series) -> None:
+ serde = JsonPlusSerializer(pickle_fallback=True)
+ dumped = serde.dumps_typed(series)
+
+ assert dumped[0] == "pickle"
+ result = serde.loads_typed(dumped)
+
+ assert result.equals(series)
diff --git a/langgraph/source/libs/checkpoint/tests/test_memory.py b/langgraph/source/libs/checkpoint/tests/test_memory.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d85f4ed56c21a9273651b489459efd9881dace0
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/tests/test_memory.py
@@ -0,0 +1,201 @@
+from typing import Any
+
+import pytest
+from langchain_core.runnables import RunnableConfig
+
+from langgraph.checkpoint.base import (
+ Checkpoint,
+ CheckpointMetadata,
+ create_checkpoint,
+ empty_checkpoint,
+)
+from langgraph.checkpoint.memory import InMemorySaver
+
+
+class TestMemorySaver:
+ @pytest.fixture(autouse=True)
+ def setup(self) -> None:
+ self.memory_saver = InMemorySaver()
+
+ # objects for test setup
+ self.config_1: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-1",
+ "checkpoint_ns": "",
+ "checkpoint_id": "1",
+ }
+ }
+ self.config_2: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-2",
+ "checkpoint_ns": "",
+ "checkpoint_id": "2",
+ }
+ }
+ self.config_3: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-2",
+ "checkpoint_id": "2-inner",
+ "checkpoint_ns": "inner",
+ }
+ }
+
+ 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,
+ }
+ self.metadata_2: CheckpointMetadata = {
+ "source": "loop",
+ "step": 1,
+ "writes": {"foo": "bar"},
+ "score": None,
+ }
+ self.metadata_3: CheckpointMetadata = {}
+
+ def test_combined_metadata(self) -> None:
+ config: RunnableConfig = {
+ "configurable": {
+ "thread_id": "thread-2",
+ "checkpoint_ns": "",
+ "__super_private_key": "super_private_value",
+ },
+ "metadata": {"run_id": "my_run_id"},
+ }
+ self.memory_saver.put(
+ config, self.chkpnt_2, self.metadata_2, self.chkpnt_2["channel_versions"]
+ )
+ checkpoint = self.memory_saver.get_tuple(config)
+ assert checkpoint is not None
+ assert checkpoint.metadata == {
+ **self.metadata_2,
+ "run_id": "my_run_id",
+ }
+
+ async def test_search(self) -> None:
+ # set up test
+ # save checkpoints
+ self.memory_saver.put(
+ self.config_1,
+ self.chkpnt_1,
+ self.metadata_1,
+ self.chkpnt_1["channel_versions"],
+ )
+ self.memory_saver.put(
+ self.config_2,
+ self.chkpnt_2,
+ self.metadata_2,
+ self.chkpnt_2["channel_versions"],
+ )
+ self.memory_saver.put(
+ self.config_3,
+ self.chkpnt_3,
+ self.metadata_3,
+ self.chkpnt_3["channel_versions"],
+ )
+
+ # 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_1 = list(self.memory_saver.list(None, filter=query_1))
+ assert len(search_results_1) == 1
+ assert search_results_1[0].metadata == self.metadata_1
+
+ search_results_2 = list(self.memory_saver.list(None, filter=query_2))
+ assert len(search_results_2) == 1
+ assert search_results_2[0].metadata == self.metadata_2
+
+ search_results_3 = list(self.memory_saver.list(None, filter=query_3))
+ assert len(search_results_3) == 3
+
+ search_results_4 = list(self.memory_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(
+ self.memory_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
+
+ async def test_asearch(self) -> None:
+ # set up test
+ # save checkpoints
+ self.memory_saver.put(
+ self.config_1,
+ self.chkpnt_1,
+ self.metadata_1,
+ self.chkpnt_1["channel_versions"],
+ )
+ self.memory_saver.put(
+ self.config_2,
+ self.chkpnt_2,
+ self.metadata_2,
+ self.chkpnt_2["channel_versions"],
+ )
+ self.memory_saver.put(
+ self.config_3,
+ self.chkpnt_3,
+ self.metadata_3,
+ self.chkpnt_3["channel_versions"],
+ )
+
+ # 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_1 = [
+ c async for c in self.memory_saver.alist(None, filter=query_1)
+ ]
+ assert len(search_results_1) == 1
+ assert search_results_1[0].metadata == self.metadata_1
+
+ search_results_2 = [
+ c async for c in self.memory_saver.alist(None, filter=query_2)
+ ]
+ assert len(search_results_2) == 1
+ assert search_results_2[0].metadata == self.metadata_2
+
+ search_results_3 = [
+ c async for c in self.memory_saver.alist(None, filter=query_3)
+ ]
+ assert len(search_results_3) == 3
+
+ search_results_4 = [
+ c async for c in self.memory_saver.alist(None, filter=query_4)
+ ]
+ assert len(search_results_4) == 0
+
+
+async def test_memory_saver() -> None:
+ from langgraph.checkpoint.memory import InMemorySaver
+
+ memory_saver = InMemorySaver()
+ assert isinstance(memory_saver, InMemorySaver)
+
+ async with memory_saver as async_memory_saver:
+ assert async_memory_saver is memory_saver
+
+ with memory_saver as sync_memory_saver:
+ assert sync_memory_saver is memory_saver
diff --git a/langgraph/source/libs/checkpoint/tests/test_redis_cache.py b/langgraph/source/libs/checkpoint/tests/test_redis_cache.py
new file mode 100644
index 0000000000000000000000000000000000000000..ca1fce09c9dc14225d18e532fe614bc8de08a71f
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/tests/test_redis_cache.py
@@ -0,0 +1,316 @@
+"""Unit tests for Redis cache implementation."""
+
+import time
+
+import pytest
+import redis
+
+from langgraph.cache.base import FullKey
+from langgraph.cache.redis import RedisCache
+
+
+class TestRedisCache:
+ @pytest.fixture(autouse=True)
+ def setup(self) -> None:
+ """Set up test Redis client and cache."""
+ self.client = redis.Redis(
+ host="localhost", port=6379, db=0, decode_responses=False
+ )
+ try:
+ self.client.ping()
+ except redis.ConnectionError:
+ pytest.skip("Redis server not available")
+
+ self.cache: RedisCache = RedisCache(self.client, prefix="test:cache:")
+
+ # Clean up before each test
+ self.client.flushdb()
+
+ def teardown_method(self) -> None:
+ """Clean up after each test."""
+ try:
+ self.client.flushdb()
+ except Exception:
+ pass
+
+ def test_basic_set_and_get(self) -> None:
+ """Test basic set and get operations."""
+ keys: list[FullKey] = [(("graph", "node"), "key1")]
+ values = {keys[0]: ({"result": 42}, None)}
+
+ # Set value
+ self.cache.set(values)
+
+ # Get value
+ result = self.cache.get(keys)
+ assert len(result) == 1
+ assert result[keys[0]] == {"result": 42}
+
+ def test_batch_operations(self) -> None:
+ """Test batch set and get operations."""
+ keys: list[FullKey] = [
+ (("graph", "node1"), "key1"),
+ (("graph", "node2"), "key2"),
+ (("other", "node"), "key3"),
+ ]
+ values = {
+ keys[0]: ({"result": 1}, None),
+ keys[1]: ({"result": 2}, 60), # With TTL
+ keys[2]: ({"result": 3}, None),
+ }
+
+ # Set values
+ self.cache.set(values)
+
+ # Get all values
+ result = self.cache.get(keys)
+ assert len(result) == 3
+ assert result[keys[0]] == {"result": 1}
+ assert result[keys[1]] == {"result": 2}
+ assert result[keys[2]] == {"result": 3}
+
+ def test_ttl_behavior(self) -> None:
+ """Test TTL (time-to-live) functionality."""
+ key: FullKey = (("graph", "node"), "ttl_key")
+ values = {key: ({"data": "expires_soon"}, 1)} # 1 second TTL
+
+ # Set with TTL
+ self.cache.set(values)
+
+ # Should be available immediately
+ result = self.cache.get([key])
+ assert len(result) == 1
+ assert result[key] == {"data": "expires_soon"}
+
+ # Wait for expiration
+ time.sleep(1.1)
+
+ # Should be expired
+ result = self.cache.get([key])
+ assert len(result) == 0
+
+ def test_namespace_isolation(self) -> None:
+ """Test that different namespaces are isolated."""
+ key1: FullKey = (("graph1", "node"), "same_key")
+ key2: FullKey = (("graph2", "node"), "same_key")
+
+ values = {key1: ({"graph": 1}, None), key2: ({"graph": 2}, None)}
+
+ self.cache.set(values)
+
+ result = self.cache.get([key1, key2])
+ assert result[key1] == {"graph": 1}
+ assert result[key2] == {"graph": 2}
+
+ def test_clear_all(self) -> None:
+ """Test clearing all cached values."""
+ keys: list[FullKey] = [
+ (("graph", "node1"), "key1"),
+ (("graph", "node2"), "key2"),
+ ]
+ values = {keys[0]: ({"result": 1}, None), keys[1]: ({"result": 2}, None)}
+
+ self.cache.set(values)
+
+ # Verify data exists
+ result = self.cache.get(keys)
+ assert len(result) == 2
+
+ # Clear all
+ self.cache.clear()
+
+ # Verify data is gone
+ result = self.cache.get(keys)
+ assert len(result) == 0
+
+ def test_clear_by_namespace(self) -> None:
+ """Test clearing cached values by namespace."""
+ keys: list[FullKey] = [
+ (("graph1", "node"), "key1"),
+ (("graph2", "node"), "key2"),
+ (("graph1", "other"), "key3"),
+ ]
+ values = {
+ keys[0]: ({"result": 1}, None),
+ keys[1]: ({"result": 2}, None),
+ keys[2]: ({"result": 3}, None),
+ }
+
+ self.cache.set(values)
+
+ # Clear only graph1 namespace
+ self.cache.clear([("graph1", "node"), ("graph1", "other")])
+
+ # graph1 should be cleared, graph2 should remain
+ result = self.cache.get(keys)
+ assert len(result) == 1
+ assert result[keys[1]] == {"result": 2}
+
+ def test_empty_operations(self) -> None:
+ """Test behavior with empty keys/values."""
+ # Empty get
+ result = self.cache.get([])
+ assert result == {}
+
+ # Empty set
+ self.cache.set({}) # Should not raise error
+
+ def test_nonexistent_keys(self) -> None:
+ """Test getting keys that don't exist."""
+ keys: list[FullKey] = [(("graph", "node"), "nonexistent")]
+ result = self.cache.get(keys)
+ assert len(result) == 0
+
+ @pytest.mark.asyncio
+ async def test_async_operations(self) -> None:
+ """Test async set and get operations with sync Redis client."""
+ # Create sync Redis client and cache (like main integration tests)
+ client = redis.Redis(host="localhost", port=6379, db=1, decode_responses=False)
+ try:
+ client.ping()
+ except Exception:
+ pytest.skip("Redis not available")
+
+ cache: RedisCache = RedisCache(client, prefix="test:async:")
+
+ keys: list[FullKey] = [(("graph", "node"), "async_key")]
+ values = {keys[0]: ({"async": True}, None)}
+
+ # Async set (delegates to sync)
+ await cache.aset(values)
+
+ # Async get (delegates to sync)
+ result = await cache.aget(keys)
+ assert len(result) == 1
+ assert result[keys[0]] == {"async": True}
+
+ # Cleanup
+ client.flushdb()
+
+ @pytest.mark.asyncio
+ async def test_async_clear(self) -> None:
+ """Test async clear operations with sync Redis client."""
+ # Create sync Redis client and cache (like main integration tests)
+ client = redis.Redis(host="localhost", port=6379, db=1, decode_responses=False)
+ try:
+ client.ping()
+ except Exception:
+ pytest.skip("Redis not available")
+
+ cache: RedisCache = RedisCache(client, prefix="test:async:")
+
+ keys: list[FullKey] = [(("graph", "node"), "key")]
+ values = {keys[0]: ({"data": "test"}, None)}
+
+ await cache.aset(values)
+
+ # Verify data exists
+ result = await cache.aget(keys)
+ assert len(result) == 1
+
+ # Clear all (delegates to sync)
+ await cache.aclear()
+
+ # Verify data is gone
+ result = await cache.aget(keys)
+ assert len(result) == 0
+
+ # Cleanup
+ client.flushdb()
+
+ def test_redis_unavailable_get(self) -> None:
+ """Test behavior when Redis is unavailable during get operations."""
+ # Create cache with non-existent Redis server
+ bad_client = redis.Redis(
+ host="nonexistent", port=9999, socket_connect_timeout=0.1
+ )
+ cache: RedisCache = RedisCache(bad_client, prefix="test:cache:")
+
+ keys: list[FullKey] = [(("graph", "node"), "key")]
+ result = cache.get(keys)
+
+ # Should return empty dict when Redis unavailable
+ assert result == {}
+
+ def test_redis_unavailable_set(self) -> None:
+ """Test behavior when Redis is unavailable during set operations."""
+ # Create cache with non-existent Redis server
+ bad_client = redis.Redis(
+ host="nonexistent", port=9999, socket_connect_timeout=0.1
+ )
+ cache: RedisCache = RedisCache(bad_client, prefix="test:cache:")
+
+ keys: list[FullKey] = [(("graph", "node"), "key")]
+ values = {keys[0]: ({"data": "test"}, None)}
+
+ # Should not raise exception when Redis unavailable
+ cache.set(values) # Should silently fail
+
+ @pytest.mark.asyncio
+ async def test_redis_unavailable_async(self) -> None:
+ """Test async behavior when Redis is unavailable."""
+ # Create sync cache with non-existent Redis server (like main integration tests)
+ bad_client = redis.Redis(
+ host="nonexistent", port=9999, socket_connect_timeout=0.1
+ )
+ cache: RedisCache = RedisCache(bad_client, prefix="test:cache:")
+
+ keys: list[FullKey] = [(("graph", "node"), "key")]
+ values = {keys[0]: ({"data": "test"}, None)}
+
+ # Should return empty dict for get (delegates to sync)
+ result = await cache.aget(keys)
+ assert result == {}
+
+ # Should not raise exception for set (delegates to sync)
+ await cache.aset(values) # Should silently fail
+
+ def test_corrupted_data_handling(self) -> None:
+ """Test handling of corrupted data in Redis."""
+ # Set some valid data first
+ keys: list[FullKey] = [(("graph", "node"), "valid_key")]
+ values = {keys[0]: ({"data": "valid"}, None)}
+ self.cache.set(values)
+
+ # Manually insert corrupted data
+ corrupted_key = self.cache._make_key(("graph", "node"), "corrupted_key")
+ self.client.set(corrupted_key, b"invalid:data:format:too:many:colons")
+
+ # Should skip corrupted entry and return only valid ones
+ all_keys: list[FullKey] = [keys[0], (("graph", "node"), "corrupted_key")]
+ result = self.cache.get(all_keys)
+
+ assert len(result) == 1
+ assert result[keys[0]] == {"data": "valid"}
+
+ def test_key_parsing_edge_cases(self) -> None:
+ """Test key parsing with edge cases."""
+ # Test empty namespace
+ key1: FullKey = ((), "empty_ns")
+ values = {key1: ({"data": "empty_ns"}, None)}
+ self.cache.set(values)
+ result = self.cache.get([key1])
+ assert result[key1] == {"data": "empty_ns"}
+
+ # Test namespace with special characters
+ key2: FullKey = (
+ ("graph:with:colons", "node-with-dashes"),
+ "key_with_underscores",
+ )
+ values = {key2: ({"data": "special_chars"}, None)}
+ self.cache.set(values)
+ result = self.cache.get([key2])
+ assert result[key2] == {"data": "special_chars"}
+
+ def test_large_data_serialization(self) -> None:
+ """Test handling of large data objects."""
+ # Create a large data structure
+ large_data = {"large_list": list(range(1000)), "nested": {"data": "x" * 1000}}
+ key: FullKey = (("graph", "node"), "large_key")
+ values = {key: (large_data, None)}
+
+ self.cache.set(values)
+ result = self.cache.get([key])
+
+ assert len(result) == 1
+ assert result[key] == large_data
diff --git a/langgraph/source/libs/checkpoint/tests/test_store.py b/langgraph/source/libs/checkpoint/tests/test_store.py
new file mode 100644
index 0000000000000000000000000000000000000000..8311a6958353d18d35b6dcf3ae289a24f80d49da
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/tests/test_store.py
@@ -0,0 +1,1047 @@
+# mypy: disable-error-code="operator"
+import asyncio
+import json
+from collections.abc import Iterable
+from datetime import datetime
+from typing import Any
+
+import pytest
+from pytest_mock import MockerFixture
+
+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, **kwargs: Any) -> None:
+ super().__init__()
+ self._store = InMemoryStore(**kwargs)
+
+ 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_resilience() -> None:
+ """Test that AsyncBatchedBaseStore recovers gracefully from task cancellation."""
+ doc = {"foo": "bar"}
+ async_store = MockAsyncBatchedStore()
+
+ await async_store.aput(("foo", "langgraph", "foo"), "bar", doc)
+
+ # Store the original task reference
+ original_task = async_store._task
+ assert original_task is not None
+ assert not original_task.done()
+
+ # Cancel the background task
+ original_task.cancel()
+ await asyncio.sleep(0.01)
+ assert original_task.cancelled()
+
+ # Perform a new operation - this should trigger _ensure_task() to create a new task
+ result = await async_store.asearch(("foo", "langgraph", "foo"))
+ assert len(result) > 0
+ assert result[0].value == doc
+
+ # Verify a new task was created
+ new_task = async_store._task
+ assert new_task is not None
+ assert new_task is not original_task
+ assert not new_task.done()
+
+ # Test that operations continue to work with the new task
+ doc2 = {"baz": "qux"}
+ await async_store.aput(("test", "namespace"), "key", doc2)
+ result2 = await async_store.aget(("test", "namespace"), "key")
+ assert result2 is not None
+ assert result2.value == doc2
+
+
+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()
+
+ class MockStore(AsyncBatchedBaseStore):
+ def batch(self, ops: Iterable[Op]) -> list[Result]:
+ raise NotImplementedError
+
+ async def abatch(self, ops: Iterable[Op]) -> list[Result]:
+ assert all(isinstance(op, GetOp) for op in ops)
+ abatch(ops)
+ return [
+ Item(
+ value={},
+ key=getattr(op, "key", ""),
+ namespace=getattr(op, "namespace", ()),
+ created_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
+ updated_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
+ )
+ for op in ops
+ ]
+
+ store = MockStore()
+
+ # concurrent calls are batched
+ results = await asyncio.gather(
+ store.aget(namespace=("a",), key="b"),
+ store.aget(namespace=("c",), key="d"),
+ )
+ assert results == [
+ Item(
+ value={},
+ key="b",
+ namespace=("a",),
+ created_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
+ updated_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
+ ),
+ Item(
+ value={},
+ key="d",
+ namespace=("c",),
+ created_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
+ updated_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
+ ),
+ ]
+ assert abatch.call_count == 1
+ assert [tuple(c.args[0]) for c in abatch.call_args_list] == [
+ (
+ GetOp(("a",), "b", refresh_ttl=True),
+ GetOp(("c",), "d", refresh_ttl=True),
+ ),
+ ]
+
+
+async def test_async_batch_store_handles_cancellation() -> None:
+ class MockStore(AsyncBatchedBaseStore):
+ def batch(self, ops: Iterable[Op]) -> list[Result]:
+ raise NotImplementedError
+
+ async def abatch(self, ops: Iterable[Op]) -> list[Result]:
+ assert all(isinstance(op, GetOp) for op in ops)
+ return [
+ Item(
+ value={},
+ key=getattr(op, "key", ""),
+ namespace=getattr(op, "namespace", ()),
+ created_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
+ updated_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
+ )
+ for op in ops
+ ]
+
+ store = MockStore()
+
+ # Simulate cancellation
+ task = asyncio.create_task(store.aget(namespace=("a",), key="b"))
+ await asyncio.sleep(0)
+ task.cancel()
+ await asyncio.sleep(0)
+
+ # Cancelling individual queries against the store should not break the store
+ result = await store.aget(namespace=("c",), key="d")
+ assert result == Item(
+ value={},
+ key="d",
+ namespace=("c",),
+ created_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
+ updated_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
+ )
+
+
+def test_list_namespaces_basic() -> None:
+ store = InMemoryStore()
+
+ namespaces = [
+ ("a", "b", "c"),
+ ("a", "b", "d", "e"),
+ ("a", "b", "d", "i"),
+ ("a", "b", "f"),
+ ("a", "c", "f"),
+ ("b", "a", "f"),
+ ("users", "123"),
+ ("users", "456", "settings"),
+ ("admin", "users", "789"),
+ ]
+
+ for i, ns in enumerate(namespaces):
+ store.put(namespace=ns, key=f"id_{i}", value={"data": f"value_{i:02d}"})
+
+ result = store.list_namespaces(prefix=("a", "b"))
+ expected = [
+ ("a", "b", "c"),
+ ("a", "b", "d", "e"),
+ ("a", "b", "d", "i"),
+ ("a", "b", "f"),
+ ]
+ assert sorted(result) == sorted(expected)
+
+ result = store.list_namespaces(suffix=("f",))
+ expected = [
+ ("a", "b", "f"),
+ ("a", "c", "f"),
+ ("b", "a", "f"),
+ ]
+ assert sorted(result) == sorted(expected)
+
+ result = store.list_namespaces(prefix=("a",), suffix=("f",))
+ expected = [
+ ("a", "b", "f"),
+ ("a", "c", "f"),
+ ]
+ assert sorted(result) == sorted(expected)
+
+ # Test max_depth
+ result = store.list_namespaces(prefix=("a", "b"), max_depth=3)
+ expected = [
+ ("a", "b", "c"),
+ ("a", "b", "d"),
+ ("a", "b", "f"),
+ ]
+ assert sorted(result) == sorted(expected)
+
+ # Test limit and offset
+ result = store.list_namespaces(prefix=("a", "b"), limit=2)
+ expected = [
+ ("a", "b", "c"),
+ ("a", "b", "d", "e"),
+ ]
+ assert result == expected
+
+ result = store.list_namespaces(prefix=("a", "b"), offset=2)
+ expected = [
+ ("a", "b", "d", "i"),
+ ("a", "b", "f"),
+ ]
+ assert result == expected
+
+ result = store.list_namespaces(prefix=("a", "*", "f"))
+ expected = [
+ ("a", "b", "f"),
+ ("a", "c", "f"),
+ ]
+ assert sorted(result) == sorted(expected)
+
+ result = store.list_namespaces(suffix=("*", "f"))
+ expected = [
+ ("a", "b", "f"),
+ ("a", "c", "f"),
+ ("b", "a", "f"),
+ ]
+ assert sorted(result) == sorted(expected)
+
+ result = store.list_namespaces(prefix=("nonexistent",))
+ assert result == []
+
+ result = store.list_namespaces(prefix=("users", "123"))
+ expected = [("users", "123")]
+ assert result == expected
+
+
+def test_list_namespaces_with_wildcards() -> None:
+ store = InMemoryStore()
+
+ namespaces = [
+ ("users", "123"),
+ ("users", "456"),
+ ("users", "789", "settings"),
+ ("admin", "users", "789"),
+ ("guests", "123"),
+ ("guests", "456", "preferences"),
+ ]
+
+ for i, ns in enumerate(namespaces):
+ store.put(namespace=ns, key=f"id_{i}", value={"data": f"value_{i:02d}"})
+
+ result = store.list_namespaces(prefix=("users", "*"))
+ expected = [
+ ("users", "123"),
+ ("users", "456"),
+ ("users", "789", "settings"),
+ ]
+ assert sorted(result) == sorted(expected)
+
+ result = store.list_namespaces(suffix=("*", "preferences"))
+ expected = [
+ ("guests", "456", "preferences"),
+ ]
+ assert result == expected
+
+ result = store.list_namespaces(prefix=("*", "users"), suffix=("*", "settings"))
+
+ assert result == []
+
+ store.put(
+ namespace=("admin", "users", "settings", "789"),
+ key="foo",
+ value={"data": "some_val"},
+ )
+ expected = [
+ ("admin", "users", "settings", "789"),
+ ]
+
+
+def test_list_namespaces_pagination() -> None:
+ store = InMemoryStore()
+
+ for i in range(20):
+ ns = ("namespace", f"sub_{i:02d}")
+ store.put(namespace=ns, key=f"id_{i:02d}", value={"data": f"value_{i:02d}"})
+
+ result = store.list_namespaces(prefix=("namespace",), limit=5, offset=0)
+ expected = [("namespace", f"sub_{i:02d}") for i in range(5)]
+ assert result == expected
+
+ result = store.list_namespaces(prefix=("namespace",), limit=5, offset=5)
+ expected = [("namespace", f"sub_{i:02d}") for i in range(5, 10)]
+ assert result == expected
+
+ result = store.list_namespaces(prefix=("namespace",), limit=5, offset=15)
+ expected = [("namespace", f"sub_{i:02d}") for i in range(15, 20)]
+ assert result == expected
+
+
+def test_list_namespaces_max_depth() -> None:
+ store = InMemoryStore()
+
+ namespaces = [
+ ("a", "b", "c", "d"),
+ ("a", "b", "c", "e"),
+ ("a", "b", "f"),
+ ("a", "g"),
+ ("h", "i", "j", "k"),
+ ]
+
+ for i, ns in enumerate(namespaces):
+ store.put(namespace=ns, key=f"id_{i}", value={"data": f"value_{i:02d}"})
+
+ result = store.list_namespaces(max_depth=2)
+ expected = [
+ ("a", "b"),
+ ("a", "g"),
+ ("h", "i"),
+ ]
+ assert sorted(result) == sorted(expected)
+
+
+def test_list_namespaces_no_conditions() -> None:
+ store = InMemoryStore()
+
+ namespaces = [
+ ("a", "b"),
+ ("c", "d"),
+ ("e", "f", "g"),
+ ]
+
+ for i, ns in enumerate(namespaces):
+ store.put(namespace=ns, key=f"id_{i}", value={"data": f"value_{i:02d}"})
+
+ result = store.list_namespaces()
+ expected = namespaces
+ assert sorted(result) == sorted(expected)
+
+
+def test_list_namespaces_empty_store() -> None:
+ store = InMemoryStore()
+
+ result = store.list_namespaces()
+ assert result == []
+
+
+async def test_cannot_put_empty_namespace() -> None:
+ store = InMemoryStore()
+ doc = {"foo": "bar"}
+
+ with pytest.raises(InvalidNamespaceError):
+ store.put((), "foo", doc)
+
+ with pytest.raises(InvalidNamespaceError):
+ await store.aput((), "foo", doc)
+
+ with pytest.raises(InvalidNamespaceError):
+ store.put(("the", "thing.about"), "foo", doc)
+
+ with pytest.raises(InvalidNamespaceError):
+ await store.aput(("the", "thing.about"), "foo", doc)
+
+ with pytest.raises(InvalidNamespaceError):
+ store.put(("some", "fun", ""), "foo", doc)
+
+ with pytest.raises(InvalidNamespaceError):
+ await store.aput(("some", "fun", ""), "foo", doc)
+
+ with pytest.raises(InvalidNamespaceError):
+ await store.aput(("langgraph", "foo"), "bar", doc)
+
+ with pytest.raises(InvalidNamespaceError):
+ store.put(("langgraph", "foo"), "bar", doc)
+
+ 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"), 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"), query="bar")[0].value == doc
+ store.delete(("foo", "langgraph", "foo"), "bar")
+ assert store.get(("foo", "langgraph", "foo"), "bar") is None
+
+ # Do the same but go past the public put api
+ await store.abatch([PutOp(("langgraph", "foo"), "bar", doc)])
+ assert (await store.aget(("langgraph", "foo"), "bar")).value == doc # type: ignore[union-attr]
+ assert (await store.asearch(("langgraph", "foo")))[0].value == doc
+ await store.adelete(("langgraph", "foo"), "bar")
+ assert (await store.aget(("langgraph", "foo"), "bar")) is None
+ store.batch([PutOp(("langgraph", "foo"), "bar", doc)])
+ assert store.get(("langgraph", "foo"), "bar").value == doc # type: ignore[union-attr]
+ assert store.search(("langgraph", "foo"))[0].value == doc
+ store.delete(("langgraph", "foo"), "bar")
+ assert store.get(("langgraph", "foo"), "bar") is None
+
+ async_store = MockAsyncBatchedStore()
+ doc = {"foo": "bar"}
+
+ with pytest.raises(InvalidNamespaceError):
+ await async_store.aput((), "foo", doc)
+
+ with pytest.raises(InvalidNamespaceError):
+ await async_store.aput(("the", "thing.about"), "foo", doc)
+
+ with pytest.raises(InvalidNamespaceError):
+ await async_store.aput(("some", "fun", ""), "foo", doc)
+
+ with pytest.raises(InvalidNamespaceError):
+ await async_store.aput(("langgraph", "foo"), "bar", doc)
+
+ await async_store.aput(("foo", "langgraph", "foo"), "bar", doc)
+ val = await async_store.aget(("foo", "langgraph", "foo"), "bar")
+ 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
+
+ await async_store.abatch([PutOp(("valid", "namespace"), "key", doc)])
+ val = await async_store.aget(("valid", "namespace"), "key")
+ assert val is not None
+ assert val.value == doc
+ 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", refresh_ttl=True) in ops
+ assert GetOp(("test",), "different", refresh_ttl=True) 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()
+
+
+@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, strict=False):
+ 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 is not None and bscore is not None
+ assert ascore == pytest.approx(bscore, abs=1e-5)
+
+ 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
+
+
+def test_non_ascii(fake_embeddings: CharacterEmbeddings) -> None:
+ """Test support for non-ascii characters"""
+ store = InMemoryStore(
+ index={"dims": fake_embeddings.dims, "embed": fake_embeddings}
+ )
+ store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
+ store.put(("user_123", "memories"), "2", {"text": "これは日本語です"}) # Japanese
+ store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
+ store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
+ store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
+
+ result1 = store.search(("user_123", "memories"), query="这是中文")
+ result2 = store.search(("user_123", "memories"), query="これは日本語です")
+ result3 = store.search(("user_123", "memories"), query="이건 한국어야")
+ result4 = store.search(("user_123", "memories"), query="Это русский")
+ result5 = store.search(("user_123", "memories"), query="यह रूसी है")
+
+ assert result1[0].key == "1"
+ assert result2[0].key == "2"
+ assert result3[0].key == "3"
+ assert result4[0].key == "4"
+ assert result5[0].key == "5"
diff --git a/langgraph/source/libs/checkpoint/uv.lock b/langgraph/source/libs/checkpoint/uv.lock
new file mode 100644
index 0000000000000000000000000000000000000000..53c4f12e5c3866da241043989f9172fae314293a
--- /dev/null
+++ b/langgraph/source/libs/checkpoint/uv.lock
@@ -0,0 +1,1545 @@
+version = 1
+revision = 3
+requires-python = ">=3.10"
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version < '3.11'",
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.12.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
+]
+
+[[package]]
+name = "async-timeout"
+version = "5.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" },
+]
+
+[[package]]
+name = "backports-asyncio-runner"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.1.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
+ { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
+ { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
+ { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
+ { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
+ { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
+ { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
+ { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
+ { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
+ { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
+ { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
+ { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
+ { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
+ { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
+ { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
+ { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
+ { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
+ { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
+ { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
+]
+
+[[package]]
+name = "codespell"
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740, upload-time = "2025-01-28T18:52:39.411Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501, upload-time = "2025-01-28T18:52:37.057Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "dataclasses-json"
+version = "0.6.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "marshmallow" },
+ { name = "typing-inspect" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "jsonpatch"
+version = "1.33"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonpointer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" },
+]
+
+[[package]]
+name = "jsonpointer"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" },
+]
+
+[[package]]
+name = "langchain-core"
+version = "1.2.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonpatch" },
+ { name = "langsmith" },
+ { name = "packaging" },
+ { name = "pydantic" },
+ { name = "pyyaml" },
+ { name = "tenacity" },
+ { name = "typing-extensions" },
+ { name = "uuid-utils" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a2/0e/664d8d81b3493e09cbab72448d2f9d693d1fa5aa2bcc488602203a9b6da0/langchain_core-1.2.7.tar.gz", hash = "sha256:e1460639f96c352b4a41c375f25aeb8d16ffc1769499fb1c20503aad59305ced", size = 837039, upload-time = "2026-01-09T17:44:25.505Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6e/6f/34a9fba14d191a67f7e2ee3dbce3e9b86d2fa7310e2c7f2c713583481bd2/langchain_core-1.2.7-py3-none-any.whl", hash = "sha256:452f4fef7a3d883357b22600788d37e3d8854ef29da345b7ac7099f33c31828b", size = 490232, upload-time = "2026-01-09T17:44:24.236Z" },
+]
+
+[[package]]
+name = "langgraph-checkpoint"
+version = "4.0.0"
+source = { editable = "." }
+dependencies = [
+ { name = "langchain-core" },
+ { name = "ormsgpack" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "codespell" },
+ { name = "dataclasses-json" },
+ { name = "mypy" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "pandas" },
+ { name = "pandas-stubs" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watcher" },
+ { name = "redis" },
+ { name = "ruff" },
+]
+lint = [
+ { name = "codespell" },
+ { name = "mypy" },
+ { name = "ruff" },
+]
+test = [
+ { name = "dataclasses-json" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "pandas" },
+ { name = "pandas-stubs" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watcher" },
+ { name = "redis" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "langchain-core", specifier = ">=0.2.38" },
+ { name = "ormsgpack", specifier = ">=1.12.0" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "codespell" },
+ { name = "dataclasses-json" },
+ { name = "mypy" },
+ { name = "numpy" },
+ { name = "pandas" },
+ { name = "pandas-stubs", specifier = ">=2.2.2.240807" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watcher" },
+ { name = "redis" },
+ { name = "ruff" },
+]
+lint = [
+ { name = "codespell" },
+ { name = "mypy" },
+ { name = "ruff" },
+]
+test = [
+ { name = "dataclasses-json" },
+ { name = "numpy" },
+ { name = "pandas" },
+ { name = "pandas-stubs", specifier = ">=2.2.2.240807" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watcher" },
+ { name = "redis" },
+]
+
+[[package]]
+name = "langsmith"
+version = "0.6.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "httpx" },
+ { name = "orjson", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "packaging" },
+ { name = "pydantic" },
+ { name = "requests" },
+ { name = "requests-toolbelt" },
+ { name = "uuid-utils" },
+ { name = "zstandard" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e7/85/9c7933052a997da1b85bc5c774f3865e9b1da1c8d71541ea133178b13229/langsmith-0.6.4.tar.gz", hash = "sha256:36f7223a01c218079fbb17da5e536ebbaf5c1468c028abe070aa3ae59bc99ec8", size = 919964, upload-time = "2026-01-15T20:02:28.873Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/66/0f/09a6637a7ba777eb307b7c80852d9ee26438e2bdafbad6fcc849ff9d9192/langsmith-0.6.4-py3-none-any.whl", hash = "sha256:ac4835860160be371042c7adbba3cb267bcf8d96a5ea976c33a8a4acad6c5486", size = 283503, upload-time = "2026-01-15T20:02:26.662Z" },
+]
+
+[[package]]
+name = "librt"
+version = "0.7.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" },
+ { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" },
+ { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" },
+ { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" },
+ { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" },
+ { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" },
+ { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" },
+ { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" },
+ { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" },
+ { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" },
+ { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" },
+ { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" },
+ { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" },
+ { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" },
+ { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" },
+ { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" },
+ { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" },
+ { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" },
+ { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" },
+ { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" },
+ { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" },
+ { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" },
+ { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" },
+ { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" },
+]
+
+[[package]]
+name = "marshmallow"
+version = "3.26.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" },
+]
+
+[[package]]
+name = "mypy"
+version = "1.19.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "mypy-extensions" },
+ { name = "pathspec" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" },
+ { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" },
+ { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" },
+ { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
+ { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
+ { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" },
+ { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
+ { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
+ { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
+ { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
+ { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" },
+ { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" },
+ { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.2.6"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" },
+ { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" },
+ { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" },
+ { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
+ { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
+ { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
+ { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
+ { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
+ { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" },
+ { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" },
+ { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
+ { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
+ { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
+ { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
+ { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
+ { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
+ { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
+ { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
+ { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
+ { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
+ { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
+ { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
+ { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
+ { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
+ { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
+ { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/57/26e5f97d075aef3794045a6ca9eada6a4ed70eb9a40e7a4a93f9ac80d704/numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", size = 12645658, upload-time = "2026-01-10T06:42:17.298Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/ba/80fc0b1e3cb2fd5c6143f00f42eb67762aa043eaa05ca924ecc3222a7849/numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", size = 5474132, upload-time = "2026-01-10T06:42:19.637Z" },
+ { url = "https://files.pythonhosted.org/packages/40/ae/0a5b9a397f0e865ec171187c78d9b57e5588afc439a04ba9cab1ebb2c945/numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", size = 6804159, upload-time = "2026-01-10T06:42:21.44Z" },
+ { url = "https://files.pythonhosted.org/packages/86/9c/841c15e691c7085caa6fd162f063eff494099c8327aeccd509d1ab1e36ab/numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", size = 14708058, upload-time = "2026-01-10T06:42:23.546Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/9d/7862db06743f489e6a502a3b93136d73aea27d97b2cf91504f70a27501d6/numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", size = 16651501, upload-time = "2026-01-10T06:42:25.909Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/9c/6fc34ebcbd4015c6e5f0c0ce38264010ce8a546cb6beacb457b84a75dfc8/numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", size = 16492627, upload-time = "2026-01-10T06:42:28.938Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/63/2494a8597502dacda439f61b3c0db4da59928150e62be0e99395c3ad23c5/numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", size = 18585052, upload-time = "2026-01-10T06:42:31.312Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/93/098e1162ae7522fc9b618d6272b77404c4656c72432ecee3abc029aa3de0/numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0", size = 6236575, upload-time = "2026-01-10T06:42:33.872Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/de/f5e79650d23d9e12f38a7bc6b03ea0835b9575494f8ec94c11c6e773b1b1/numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574", size = 12604479, upload-time = "2026-01-10T06:42:35.778Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/65/e1097a7047cff12ce3369bd003811516b20ba1078dbdec135e1cd7c16c56/numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73", size = 10578325, upload-time = "2026-01-10T06:42:38.518Z" },
+ { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" },
+ { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" },
+ { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" },
+ { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" },
+ { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" },
+ { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" },
+ { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495, upload-time = "2026-01-10T06:43:06.283Z" },
+ { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657, upload-time = "2026-01-10T06:43:09.094Z" },
+ { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256, upload-time = "2026-01-10T06:43:13.634Z" },
+ { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212, upload-time = "2026-01-10T06:43:15.661Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871, upload-time = "2026-01-10T06:43:17.324Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305, upload-time = "2026-01-10T06:43:19.376Z" },
+ { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909, upload-time = "2026-01-10T06:43:21.808Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380, upload-time = "2026-01-10T06:43:23.957Z" },
+ { url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089, upload-time = "2026-01-10T06:43:27.535Z" },
+ { url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230, upload-time = "2026-01-10T06:43:29.298Z" },
+ { url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125, upload-time = "2026-01-10T06:43:31.782Z" },
+ { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156, upload-time = "2026-01-10T06:43:34.237Z" },
+ { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663, upload-time = "2026-01-10T06:43:36.211Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224, upload-time = "2026-01-10T06:43:37.884Z" },
+ { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352, upload-time = "2026-01-10T06:43:39.479Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279, upload-time = "2026-01-10T06:43:41.93Z" },
+ { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316, upload-time = "2026-01-10T06:43:44.121Z" },
+ { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884, upload-time = "2026-01-10T06:43:46.613Z" },
+ { url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138, upload-time = "2026-01-10T06:43:48.854Z" },
+ { url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478, upload-time = "2026-01-10T06:43:50.476Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981, upload-time = "2026-01-10T06:43:52.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" },
+ { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722, upload-time = "2026-01-10T06:44:13.332Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590, upload-time = "2026-01-10T06:44:15.006Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180, upload-time = "2026-01-10T06:44:17.386Z" },
+ { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" },
+ { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" },
+ { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" },
+ { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/48/d86f97919e79314a1cdee4c832178763e6e98e623e123d0bada19e92c15a/numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", size = 16822202, upload-time = "2026-01-10T06:44:43.738Z" },
+ { url = "https://files.pythonhosted.org/packages/51/e9/1e62a7f77e0f37dcfb0ad6a9744e65df00242b6ea37dfafb55debcbf5b55/numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", size = 12569985, upload-time = "2026-01-10T06:44:45.945Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/7e/914d54f0c801342306fdcdce3e994a56476f1b818c46c47fc21ae968088c/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", size = 5398484, upload-time = "2026-01-10T06:44:48.012Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/d8/9570b68584e293a33474e7b5a77ca404f1dcc655e40050a600dee81d27fb/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", size = 6713216, upload-time = "2026-01-10T06:44:49.725Z" },
+ { url = "https://files.pythonhosted.org/packages/33/9b/9dd6e2db8d49eb24f86acaaa5258e5f4c8ed38209a4ee9de2d1a0ca25045/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", size = 14538937, upload-time = "2026-01-10T06:44:51.498Z" },
+ { url = "https://files.pythonhosted.org/packages/53/87/d5bd995b0f798a37105b876350d346eea5838bd8f77ea3d7a48392f3812b/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", size = 16479830, upload-time = "2026-01-10T06:44:53.931Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/c7/b801bf98514b6ae6475e941ac05c58e6411dd863ea92916bfd6d510b08c1/numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33", size = 12492579, upload-time = "2026-01-10T06:44:57.094Z" },
+]
+
+[[package]]
+name = "orjson"
+version = "3.11.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/19/b22cf9dad4db20c8737041046054cbd4f38bb5a2d0e4bb60487832ce3d76/orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1", size = 245719, upload-time = "2025-12-06T15:53:43.877Z" },
+ { url = "https://files.pythonhosted.org/packages/03/2e/b136dd6bf30ef5143fbe76a4c142828b55ccc618be490201e9073ad954a1/orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870", size = 132467, upload-time = "2025-12-06T15:53:45.379Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/fc/ae99bfc1e1887d20a0268f0e2686eb5b13d0ea7bbe01de2b566febcd2130/orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09", size = 130702, upload-time = "2025-12-06T15:53:46.659Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/43/ef7912144097765997170aca59249725c3ab8ef6079f93f9d708dd058df5/orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd", size = 135907, upload-time = "2025-12-06T15:53:48.487Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/da/24d50e2d7f4092ddd4d784e37a3fa41f22ce8ed97abc9edd222901a96e74/orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac", size = 139935, upload-time = "2025-12-06T15:53:49.88Z" },
+ { url = "https://files.pythonhosted.org/packages/02/4a/b4cb6fcbfff5b95a3a019a8648255a0fac9b221fbf6b6e72be8df2361feb/orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e", size = 137541, upload-time = "2025-12-06T15:53:51.226Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/99/a11bd129f18c2377c27b2846a9d9be04acec981f770d711ba0aaea563984/orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f", size = 139031, upload-time = "2025-12-06T15:53:52.309Z" },
+ { url = "https://files.pythonhosted.org/packages/64/29/d7b77d7911574733a036bb3e8ad7053ceb2b7d6ea42208b9dbc55b23b9ed/orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18", size = 141622, upload-time = "2025-12-06T15:53:53.606Z" },
+ { url = "https://files.pythonhosted.org/packages/93/41/332db96c1de76b2feda4f453e91c27202cd092835936ce2b70828212f726/orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a", size = 413800, upload-time = "2025-12-06T15:53:54.866Z" },
+ { url = "https://files.pythonhosted.org/packages/76/e1/5a0d148dd1f89ad2f9651df67835b209ab7fcb1118658cf353425d7563e9/orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7", size = 151198, upload-time = "2025-12-06T15:53:56.383Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/96/8db67430d317a01ae5cf7971914f6775affdcfe99f5bff9ef3da32492ecc/orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401", size = 141984, upload-time = "2025-12-06T15:53:57.746Z" },
+ { url = "https://files.pythonhosted.org/packages/71/49/40d21e1aa1ac569e521069228bb29c9b5a350344ccf922a0227d93c2ed44/orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8", size = 135272, upload-time = "2025-12-06T15:53:59.769Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/7e/d0e31e78be0c100e08be64f48d2850b23bcb4d4c70d114f4e43b39f6895a/orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167", size = 133360, upload-time = "2025-12-06T15:54:01.25Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" },
+ { url = "https://files.pythonhosted.org/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c6/0a8caff96f4503f4f7dd44e40e90f4d14acf80d3b7a97cb88747bb712d3e/orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7", size = 130516, upload-time = "2025-12-06T15:54:06.274Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/63/43d4dc9bd9954bff7052f700fdb501067f6fb134a003ddcea2a0bb3854ed/orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd", size = 135695, upload-time = "2025-12-06T15:54:07.702Z" },
+ { url = "https://files.pythonhosted.org/packages/87/6f/27e2e76d110919cb7fcb72b26166ee676480a701bcf8fc53ac5d0edce32f/orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9", size = 139664, upload-time = "2025-12-06T15:54:08.828Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/f8/5966153a5f1be49b5fbb8ca619a529fde7bc71aa0a376f2bb83fed248bcd/orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef", size = 137289, upload-time = "2025-12-06T15:54:09.898Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/34/8acb12ff0299385c8bbcbb19fbe40030f23f15a6de57a9c587ebf71483fb/orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9", size = 138784, upload-time = "2025-12-06T15:54:11.022Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/27/910421ea6e34a527f73d8f4ee7bdffa48357ff79c7b8d6eb6f7b82dd1176/orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125", size = 141322, upload-time = "2025-12-06T15:54:12.427Z" },
+ { url = "https://files.pythonhosted.org/packages/87/a3/4b703edd1a05555d4bb1753d6ce44e1a05b7a6d7c164d5b332c795c63d70/orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814", size = 413612, upload-time = "2025-12-06T15:54:13.858Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/36/034177f11d7eeea16d3d2c42a1883b0373978e08bc9dad387f5074c786d8/orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5", size = 150993, upload-time = "2025-12-06T15:54:15.189Z" },
+ { url = "https://files.pythonhosted.org/packages/44/2f/ea8b24ee046a50a7d141c0227c4496b1180b215e728e3b640684f0ea448d/orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880", size = 141774, upload-time = "2025-12-06T15:54:16.451Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/12/cc440554bf8200eb23348a5744a575a342497b65261cd65ef3b28332510a/orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d", size = 135109, upload-time = "2025-12-06T15:54:17.73Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/83/e0c5aa06ba73a6760134b169f11fb970caa1525fa4461f94d76e692299d9/orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1", size = 133193, upload-time = "2025-12-06T15:54:19.426Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/35/5b77eaebc60d735e832c5b1a20b155667645d123f09d471db0a78280fb49/orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c", size = 126830, upload-time = "2025-12-06T15:54:20.836Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" },
+ { url = "https://files.pythonhosted.org/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" },
+ { url = "https://files.pythonhosted.org/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" },
+ { url = "https://files.pythonhosted.org/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" },
+ { url = "https://files.pythonhosted.org/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" },
+ { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" },
+ { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" },
+ { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" },
+ { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" },
+ { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" },
+ { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" },
+ { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" },
+ { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" },
+ { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" },
+ { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" },
+ { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" },
+ { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" },
+ { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" },
+ { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" },
+]
+
+[[package]]
+name = "ormsgpack"
+version = "1.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/96/34c40d621996c2f377a18decbd3c59f031dde73c3ba47d1e1e8f29a05aaa/ormsgpack-1.12.1.tar.gz", hash = "sha256:a3877fde1e4f27a39f92681a0aab6385af3a41d0c25375d33590ae20410ea2ac", size = 39476, upload-time = "2025-12-14T07:57:43.248Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/da/caf25cc54d6870089a0b5614c4c5914dd3fae45f9f7f84a32445ad0612e3/ormsgpack-1.12.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:62e3614cab63fa5aa42f5f0ca3cd12899f0bfc5eb8a5a0ebab09d571c89d427d", size = 376182, upload-time = "2025-12-14T07:56:46.094Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/02/ccc9170c6bee86f428707f15b5ad68d42c71d43856e1b8e37cdfea50af5b/ormsgpack-1.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86d9fbf85c05c69c33c229d2eba7c8c3500a56596cd8348131c918acd040d6af", size = 202339, upload-time = "2025-12-14T07:56:47.609Z" },
+ { url = "https://files.pythonhosted.org/packages/86/c7/10309a5a6421adaedab710a72470143d664bb0a043cc095c1311878325a0/ormsgpack-1.12.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8d246e66f09d8e0f96e770829149ee83206e90ed12f5987998bb7be84aec99fe", size = 210720, upload-time = "2025-12-14T07:56:48.66Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/b4/92a0f7a00c5f0c71b51dc3112e53b1ca937b9891a08979d06524db11b799/ormsgpack-1.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfc2c830a1ed2d00de713d08c9e62efa699e8fd29beafa626aaebe466f583ebb", size = 211264, upload-time = "2025-12-14T07:56:49.976Z" },
+ { url = "https://files.pythonhosted.org/packages/33/fa/5cce85c8e58fcaa048c75fbbe37816a1b3fb58ba4289a7dedc4f4ed9ce82/ormsgpack-1.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc892757d8f9eea5208268a527cf93c98409802f6a9f7c8d71a7b8f9ba5cb944", size = 386076, upload-time = "2025-12-14T07:56:51.74Z" },
+ { url = "https://files.pythonhosted.org/packages/88/d0/f18d258c733eb22eadad748659f7984d0b6a851fb3deefcb33f50e9a947a/ormsgpack-1.12.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0de1dbcf11ea739ac4a882b43d5c2055e6d99ce64e8d6502e25d6d881700c017", size = 479570, upload-time = "2025-12-14T07:56:52.912Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/3a/b362dff090f4740090fe51d512f24b1e320d1f96497ebf9248e2a04ac88f/ormsgpack-1.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d5065dfb9ec4db93241c60847624d9aeef4ccb449c26a018c216b55c69be83c0", size = 387859, upload-time = "2025-12-14T07:56:53.968Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/8a/d948965598b2b7872800076da5c02573aa72f716be57a3d4fe60490b2a2a/ormsgpack-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d17103c4726181d7000c61b751c881f1b6f401d146df12da028fc730227df19", size = 115906, upload-time = "2025-12-14T07:56:55.068Z" },
+ { url = "https://files.pythonhosted.org/packages/57/e2/f5b89365c8dc8025c27d31316038f1c103758ddbf87dc0fa8e3f78f66907/ormsgpack-1.12.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4038f59ae0e19dac5e5d9aae4ec17ff84a79e046342ee73ccdecf3547ecf0d34", size = 376180, upload-time = "2025-12-14T07:56:56.521Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/87/3f694e06f5e32c6d65066f53b4a025282a5072b6b336c17560b00e04606d/ormsgpack-1.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16c63b0c5a3eec467e4bb33a14dabba076b7d934dff62898297b5c0b5f7c3cb3", size = 202338, upload-time = "2025-12-14T07:56:57.585Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/f5/6d95d7b7c11f97a92522082fc7e5d1ab34537929f1e13f4c369f392f19d0/ormsgpack-1.12.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:74fd6a8e037eb310dda865298e8d122540af00fe5658ec18b97a1d34f4012e4d", size = 210720, upload-time = "2025-12-14T07:56:58.968Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/9d/9a49a2686f8b7165dcb2342b8554951263c30c0f0825f1fcc2d56e736a6b/ormsgpack-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58ad60308e233dd824a1859eabb5fe092e123e885eafa4ad5789322329c80fb5", size = 211264, upload-time = "2025-12-14T07:57:00.099Z" },
+ { url = "https://files.pythonhosted.org/packages/02/31/2fdc36eaeca2182900b96fc7b19755f293283fe681750e3d295733d62f0e/ormsgpack-1.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:35127464c941c1219acbe1a220e48d55e7933373d12257202f4042f7044b4c90", size = 386081, upload-time = "2025-12-14T07:57:01.177Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/65/0a765432f08ae26b4013c6a9aed97be17a9ef85f1600948a474b518e27dd/ormsgpack-1.12.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c48d1c50794692d1e6e3f8c3bb65f5c3acfaae9347e506484a65d60b3d91fb50", size = 479572, upload-time = "2025-12-14T07:57:02.738Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/4f/f2f15ebef786ad71cea420bf8692448fbddf04d1bf3feaa68bd5ee3172e6/ormsgpack-1.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b512b2ad6feaaefdc26e05431ed2843e42483041e354e167c53401afaa83d919", size = 387862, upload-time = "2025-12-14T07:57:03.842Z" },
+ { url = "https://files.pythonhosted.org/packages/15/eb/86fbef1d605fa91ecef077f93f9d0e34fc39b23475dfe3ffb92f6c8db28d/ormsgpack-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:93f30db95e101a9616323bfc50807ad00e7f6197cea2216d2d24af42afc77d88", size = 115900, upload-time = "2025-12-14T07:57:05.137Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/67/7ba1a46e6a6e263fc42a4fafc24afc1ab21a66116553cad670426f0bd9ef/ormsgpack-1.12.1-cp311-cp311-win_arm64.whl", hash = "sha256:d75b5fa14f6abffce2c392ee03b4731199d8a964c81ee8645c4c79af0e80fd50", size = 109868, upload-time = "2025-12-14T07:57:06.834Z" },
+ { url = "https://files.pythonhosted.org/packages/17/fe/ab9167ca037406b5703add24049cf3e18021a3b16133ea20615b1f160ea4/ormsgpack-1.12.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4d7fb0e1b6fbc701d75269f7405a4f79230a6ce0063fb1092e4f6577e312f86d", size = 376725, upload-time = "2025-12-14T07:57:07.894Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/ea/2820e65f506894c459b840d1091ae6e327fde3d5a3f3b002a11a1b9bdf7d/ormsgpack-1.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43a9353e2db5b024c91a47d864ef15eaa62d81824cfc7740fed4cef7db738694", size = 202466, upload-time = "2025-12-14T07:57:09.049Z" },
+ { url = "https://files.pythonhosted.org/packages/45/8b/def01c13339c5bbec2ee1469ef53e7fadd66c8d775df974ee4def1572515/ormsgpack-1.12.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc8fe866b7706fc25af0adf1f600bc06ece5b15ca44e34641327198b821e5c3c", size = 210748, upload-time = "2025-12-14T07:57:10.074Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/d2/bf350c92f7f067dd9484499705f2d8366d8d9008a670e3d1d0add1908f85/ormsgpack-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:813755b5f598a78242042e05dfd1ada4e769e94b98c9ab82554550f97ff4d641", size = 211510, upload-time = "2025-12-14T07:57:11.165Z" },
+ { url = "https://files.pythonhosted.org/packages/74/92/9d689bcb95304a6da26c4d59439c350940c25d1b35f146d402ccc6344c51/ormsgpack-1.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8eea2a13536fae45d78f93f2cc846c9765c7160c85f19cfefecc20873c137cdd", size = 386237, upload-time = "2025-12-14T07:57:12.306Z" },
+ { url = "https://files.pythonhosted.org/packages/17/fe/bd3107547f8b6129265dd957f40b9cd547d2445db2292aacb13335a7ea89/ormsgpack-1.12.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7a02ebda1a863cbc604740e76faca8eee1add322db2dcbe6cf32669fffdff65c", size = 479589, upload-time = "2025-12-14T07:57:13.475Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/7c/e8e5cc9edb967d44f6f85e9ebdad440b59af3fae00b137a4327dc5aed9bb/ormsgpack-1.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c0bd63897c439931cdf29348e5e6e8c330d529830e848d10767615c0f3d1b82", size = 388077, upload-time = "2025-12-14T07:57:14.551Z" },
+ { url = "https://files.pythonhosted.org/packages/35/6b/5031797e43b58506f28a8760b26dc23f2620fb4f2200c4c1b3045603e67e/ormsgpack-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:362f2e812f8d7035dc25a009171e09d7cc97cb30d3c9e75a16aeae00ca3c1dcf", size = 116190, upload-time = "2025-12-14T07:57:15.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/fd/9f43ea6425e383a6b2dbfafebb06fd60e8d68c700ef715adfbcdb499f75d/ormsgpack-1.12.1-cp312-cp312-win_arm64.whl", hash = "sha256:6190281e381db2ed0045052208f47a995ccf61eed48f1215ae3cce3fbccd59c5", size = 109990, upload-time = "2025-12-14T07:57:16.419Z" },
+ { url = "https://files.pythonhosted.org/packages/11/42/f110dfe7cf23a52a82e23eb23d9a6a76ae495447d474686dfa758f3d71d6/ormsgpack-1.12.1-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9663d6b3ecc917c063d61a99169ce196a80f3852e541ae404206836749459279", size = 376746, upload-time = "2025-12-14T07:57:17.699Z" },
+ { url = "https://files.pythonhosted.org/packages/11/76/b386e508a8ae207daec240201a81adb26467bf99b163560724e86bd9ff33/ormsgpack-1.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32e85cfbaf01a94a92520e7fe7851cfcfe21a5698299c28ab86194895f9b9233", size = 202489, upload-time = "2025-12-14T07:57:18.807Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/0e/5db7a63f387149024572daa3d9512fe8fb14bf4efa0722d6d491bed280e7/ormsgpack-1.12.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dabfd2c24b59c7c69870a5ecee480dfae914a42a0c2e7c9d971cf531e2ba471a", size = 210757, upload-time = "2025-12-14T07:57:19.893Z" },
+ { url = "https://files.pythonhosted.org/packages/64/79/3a9899e57cb57430bd766fc1b4c9ad410cb2ba6070bc8cf6301e7d385768/ormsgpack-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51bbf2b64afeded34ccd8e25402e4bca038757913931fa0d693078d75563f6f9", size = 211518, upload-time = "2025-12-14T07:57:20.972Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/cd/4f41710ae9fe50d7fcbe476793b3c487746d0e1cc194cc0fee42ff6d989b/ormsgpack-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9959a71dde1bd0ced84af17facc06a8afada495a34e9cb1bad8e9b20d4c59cef", size = 386251, upload-time = "2025-12-14T07:57:22.099Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/54/ba0c97d6231b1f01daafaa520c8cce1e1b7fceaae6fdc1c763925874a7de/ormsgpack-1.12.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:e9be0e3b62d758f21f5b20e0e06b3a240ec546c4a327bf771f5825462aa74714", size = 479607, upload-time = "2025-12-14T07:57:23.525Z" },
+ { url = "https://files.pythonhosted.org/packages/18/75/19a9a97a462776d525baf41cfb7072734528775f0a3d5fbfab3aa7756b9b/ormsgpack-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a29d49ab7fdd77ea787818e60cb4ef491708105b9c4c9b0f919201625eb036b5", size = 388062, upload-time = "2025-12-14T07:57:24.616Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/6a/ec26e3f44e9632ecd2f43638b7b37b500eaea5d79cab984ad0b94be14f82/ormsgpack-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:c418390b47a1d367e803f6c187f77e4d67c7ae07ba962e3a4a019001f4b0291a", size = 116195, upload-time = "2025-12-14T07:57:25.626Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/64/bfa5f4a34d0f15c6aba1b73e73f7441a66d635bd03249d334a4796b7a924/ormsgpack-1.12.1-cp313-cp313-win_arm64.whl", hash = "sha256:cfa22c91cffc10a7fbd43729baff2de7d9c28cef2509085a704168ae31f02568", size = 109986, upload-time = "2025-12-14T07:57:26.569Z" },
+ { url = "https://files.pythonhosted.org/packages/87/0e/78e5697164e3223b9b216c13e99f1acbc1ee9833490d68842b13da8ba883/ormsgpack-1.12.1-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b93c91efb1a70751a1902a5b43b27bd8fd38e0ca0365cf2cde2716423c15c3a6", size = 376758, upload-time = "2025-12-14T07:57:27.641Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0e/3a3cbb64703263d7bbaed7effa3ce78cb9add360a60aa7c544d7df28b641/ormsgpack-1.12.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf0ea0389167b5fa8d2933dd3f33e887ec4ba68f89c25214d7eec4afd746d22", size = 202487, upload-time = "2025-12-14T07:57:29.051Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/2c/807ebe2b77995599bbb1dec8c3f450d5d7dddee14ce3e1e71dc60e2e2a74/ormsgpack-1.12.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4c29af837f35af3375070689e781161e7cf019eb2f7cd641734ae45cd001c0d", size = 210853, upload-time = "2025-12-14T07:57:30.508Z" },
+ { url = "https://files.pythonhosted.org/packages/25/57/2cdfc354e3ad8e847628f511f4d238799d90e9e090941e50b9d5ba955ae2/ormsgpack-1.12.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:336fc65aa0fe65896a3dabaae31e332a0a98b4a00ad7b0afde21a7505fd23ff3", size = 211545, upload-time = "2025-12-14T07:57:31.585Z" },
+ { url = "https://files.pythonhosted.org/packages/76/1d/c6fda560e4a8ff865b3aec8a86f7c95ab53f4532193a6ae4ab9db35f85aa/ormsgpack-1.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:940f60aabfefe71dd6b82cb33f4ff10b2e7f5fcfa5f103cdb0a23b6aae4c713c", size = 386333, upload-time = "2025-12-14T07:57:32.957Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/3e/715081b36fceb8b497c68b87d384e1cc6d9c9c130ce3b435634d3d785b86/ormsgpack-1.12.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:596ad9e1b6d4c95595c54aaf49b1392609ca68f562ce06f4f74a5bc4053bcda4", size = 479701, upload-time = "2025-12-14T07:57:34.686Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/cf/01ad04def42b3970fc1a302c07f4b46339edf62ef9650247097260471f40/ormsgpack-1.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:575210e8fcbc7b0375026ba040a5eef223e9f66a4453d9623fc23282ae09c3c8", size = 388148, upload-time = "2025-12-14T07:57:35.771Z" },
+ { url = "https://files.pythonhosted.org/packages/15/91/1fff2fc2b5943c740028f339154e7103c8f2edf1a881d9fbba2ce11c3b1d/ormsgpack-1.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:647daa3718572280893456be44c60aea6690b7f2edc54c55648ee66e8f06550f", size = 116201, upload-time = "2025-12-14T07:57:36.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/66/142b542aed3f96002c7d1c33507ca6e1e0d0a42b9253ab27ef7ed5793bd9/ormsgpack-1.12.1-cp314-cp314-win_arm64.whl", hash = "sha256:a8b3ab762a6deaf1b6490ab46dda0c51528cf8037e0246c40875c6fe9e37b699", size = 110029, upload-time = "2025-12-14T07:57:37.703Z" },
+ { url = "https://files.pythonhosted.org/packages/38/b3/ef4494438c90359e1547eaed3c5ec46e2c431d59a3de2af4e70ebd594c49/ormsgpack-1.12.1-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:12087214e436c1f6c28491949571abea759a63111908c4f7266586d78144d7a8", size = 376777, upload-time = "2025-12-14T07:57:38.795Z" },
+ { url = "https://files.pythonhosted.org/packages/05/a0/1149a7163f8b0dfbc64bf9099b6f16d102ad3b03bcc11afee198d751da2d/ormsgpack-1.12.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6d54c14cf86ef13f10ccade94d1e7de146aa9b17d371e18b16e95f329393b7", size = 202490, upload-time = "2025-12-14T07:57:40.168Z" },
+ { url = "https://files.pythonhosted.org/packages/68/82/f2ec5e758d6a7106645cca9bb7137d98bce5d363789fa94075be6572057c/ormsgpack-1.12.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f3584d07882b7ea2a1a589f795a3af97fe4c2932b739408e6d1d9d286cad862", size = 211733, upload-time = "2025-12-14T07:57:42.253Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[package]]
+name = "pandas"
+version = "2.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "python-dateutil" },
+ { name = "pytz" },
+ { name = "tzdata" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" },
+ { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" },
+ { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" },
+ { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" },
+ { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" },
+ { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" },
+ { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" },
+ { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" },
+ { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" },
+ { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" },
+ { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" },
+ { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" },
+ { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" },
+ { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" },
+ { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" },
+ { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" },
+ { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" },
+ { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" },
+ { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" },
+ { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" },
+ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
+]
+
+[[package]]
+name = "pandas-stubs"
+version = "2.3.3.260113"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "types-pytz" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/92/5d/be23854a73fda69f1dbdda7bc10fbd6f930bd1fa87aaec389f00c901c1e8/pandas_stubs-2.3.3.260113.tar.gz", hash = "sha256:076e3724bcaa73de78932b012ec64b3010463d377fa63116f4e6850643d93800", size = 116131, upload-time = "2026-01-13T22:30:16.704Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/c6/df1fe324248424f77b89371116dab5243db7f052c32cc9fe7442ad9c5f75/pandas_stubs-2.3.3.260113-py3-none-any.whl", hash = "sha256:ec070b5c576e1badf12544ae50385872f0631fc35d99d00dc598c2954ec564d3", size = 168246, upload-time = "2026-01-13T22:30:15.244Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "1.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" },
+ { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" },
+ { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" },
+ { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+ { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
+ { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" },
+ { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" },
+ { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
+ { name = "pytest" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
+]
+
+[[package]]
+name = "pytest-mock"
+version = "3.15.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
+]
+
+[[package]]
+name = "pytest-watcher"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "watchdog" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e6/d2/80606077b7fa8784417687f494ff801d7ab817d9a17fc94305811d5919bb/pytest_watcher-0.6.3.tar.gz", hash = "sha256:842dc904264df0ad2d5264153a66bb452fccfa46598cd6e0a5ef1d19afed9b13", size = 601878, upload-time = "2026-01-10T23:28:18.805Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/3f/172d73600ad2771774cda108efb813fc724fc345e5240a81a1085f1ade5d/pytest_watcher-0.6.3-py3-none-any.whl", hash = "sha256:83e7748c933087e8276edb6078663e6afa9926434b4fd8b85cf6b32b1d5bec89", size = 12431, upload-time = "2026-01-10T23:28:17.64Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "pytz"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
+ { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
+ { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "redis"
+version = "7.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "async-timeout", marker = "python_full_version < '3.11.3'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+]
+
+[[package]]
+name = "requests-toolbelt"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.14.13"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" },
+ { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" },
+ { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" },
+ { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" },
+ { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "tenacity"
+version = "9.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
+ { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
+ { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
+ { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
+ { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
+ { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
+ { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
+ { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
+ { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
+ { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
+ { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
+ { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
+ { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
+]
+
+[[package]]
+name = "types-pytz"
+version = "2025.2.0.20251108"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/40/ff/c047ddc68c803b46470a357454ef76f4acd8c1088f5cc4891cdd909bfcf6/types_pytz-2025.2.0.20251108.tar.gz", hash = "sha256:fca87917836ae843f07129567b74c1929f1870610681b4c92cb86a3df5817bdb", size = 10961, upload-time = "2025-11-08T02:55:57.001Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/c1/56ef16bf5dcd255155cc736d276efa6ae0a5c26fd685e28f0412a4013c01/types_pytz-2025.2.0.20251108-py3-none-any.whl", hash = "sha256:0f1c9792cab4eb0e46c52f8845c8f77cf1e313cb3d68bf826aa867fe4717d91c", size = 10116, upload-time = "2025-11-08T02:55:56.194Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspect"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mypy-extensions" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
+]
+
+[[package]]
+name = "uuid-utils"
+version = "0.13.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/8a/17b11768dcb473d3a255c02ffdd94fbd1b345c906efea0a39124dcbaed52/uuid_utils-0.13.0.tar.gz", hash = "sha256:4c17df6427a9e23a4cd7fb9ee1efb53b8abb078660b9bdb2524ca8595022dfe1", size = 21921, upload-time = "2026-01-08T15:48:10.841Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/b8/d40848ca22781f206c60a1885fc737d2640392bd6b5792d455525accd89c/uuid_utils-0.13.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:83628283e977fb212e756bc055df8fdd2f9f589a2e539ba1abe755b8ce8df7a4", size = 602130, upload-time = "2026-01-08T15:47:34.877Z" },
+ { url = "https://files.pythonhosted.org/packages/40/b9/00a944b8096632ea12638181f8e294abcde3e3b8b5e29b777f809896f6ae/uuid_utils-0.13.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c47638ed6334ab19d80f73664f153b04bbb04ab8ce4298d10da6a292d4d21c47", size = 304213, upload-time = "2026-01-08T15:47:36.807Z" },
+ { url = "https://files.pythonhosted.org/packages/da/d7/07b36c33aef683b81c9afff3ec178d5eb39d325447a68c3c68a62e4abb32/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:b276b538c57733ed406948584912da422a604313c71479654848b84b9e19c9b0", size = 340624, upload-time = "2026-01-08T15:47:38.821Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/55/fcff2fff02a27866cb1a6614c9df2b3ace721f0a0aab2b7b8f5a7d4e4221/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_armv7l.whl", hash = "sha256:bdaf2b77e34b199cf04cde28399495fd1ed951de214a4ece1f3919b2f945bb06", size = 346705, upload-time = "2026-01-08T15:47:40.397Z" },
+ { url = "https://files.pythonhosted.org/packages/41/48/67438506c2bb8bee1b4b00d7c0b3ff866401b4790849bf591d654d4ea0bc/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_i686.whl", hash = "sha256:eb2f0baf81e82f9769a7684022dca8f3bf801ca1574a3e94df1876e9d6f9271e", size = 366023, upload-time = "2026-01-08T15:47:42.662Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/d7/2d91ce17f62fd764d593430de296b70843cc25229c772453f7261de9e6a8/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_ppc64le.whl", hash = "sha256:6be6c4d11275f5cc402a4fdba6c2b1ce45fd3d99bb78716cd1cc2cbf6802b2ce", size = 471149, upload-time = "2026-01-08T15:47:44.963Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/9a/aa0756186073ba84daf5704c150d41ede10eb3185d510e02532e2071550e/uuid_utils-0.13.0-cp39-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:77621cf6ceca7f42173a642a01c01c216f9eaec3b7b65d093d2d6a433ca0a83d", size = 342130, upload-time = "2026-01-08T15:47:46.331Z" },
+ { url = "https://files.pythonhosted.org/packages/74/b4/3191789f4dc3bed59d79cec90559821756297a25d7dc34d1bf7781577a75/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a5a9eb06c2bb86dd876cd7b2fe927fc8543d14c90d971581db6ffda4a02526f", size = 524128, upload-time = "2026-01-08T15:47:47.628Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/30/29839210a8fff9fc219bfa7c8d8cd115324e92618cba0cda090d54d3d321/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:775347c6110fb71360df17aac74132d8d47c1dbe71233ac98197fc872a791fd2", size = 615872, upload-time = "2026-01-08T15:47:50.61Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ed/15000c96a8bd8f5fd8efd622109bf52549ea0b366f8ce71c45580fa55878/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf95f6370ad1a0910ee7b5ad5228fd19c4ae32fe3627389006adaf519408c41e", size = 581023, upload-time = "2026-01-08T15:47:52.776Z" },
+ { url = "https://files.pythonhosted.org/packages/67/c8/3f809fa2dc2ca4bd331c792a3c7d3e45ae2b709d85847a12b8b27d1d5f19/uuid_utils-0.13.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a88e23e0b2f4203fefe2ccbca5736ee06fcad10e61b5e7e39c8d7904bc13300", size = 546715, upload-time = "2026-01-08T15:47:54.415Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/80/4f7c7efd734d1494397c781bd3d421688e9c187ae836e3174625b1ddf8b0/uuid_utils-0.13.0-cp39-abi3-win32.whl", hash = "sha256:3e4f2cc54e6a99c0551158100ead528479ad2596847478cbad624977064ffce3", size = 177650, upload-time = "2026-01-08T15:47:55.679Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/94/d05ab68622e66ad787a241dfe5ccc649b3af09f30eae977b9ee8f7046aaa/uuid_utils-0.13.0-cp39-abi3-win_amd64.whl", hash = "sha256:046cb2756e1597b3de22d24851b769913e192135830486a0a70bf41327f0360c", size = 183211, upload-time = "2026-01-08T15:47:57.604Z" },
+ { url = "https://files.pythonhosted.org/packages/69/37/674b3ce25cd715b831ea8ebbd828b74c40159f04c95d1bb963b2c876fe79/uuid_utils-0.13.0-cp39-abi3-win_arm64.whl", hash = "sha256:5447a680df6ef8a5a353976aaf4c97cc3a3a22b1ee13671c44227b921e3ae2a9", size = 183518, upload-time = "2026-01-08T15:47:59.148Z" },
+ { url = "https://files.pythonhosted.org/packages/99/fa/1d92de9538463859228e68db679b766fd300770c9a2db849dcba0c0c5a57/uuid_utils-0.13.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e5182e2d95f38e65f2e5bce90648ef56987443da13e145afcd747e584f9bc69c", size = 587641, upload-time = "2026-01-08T15:48:00.433Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/07/6bd9e6f5367e38c2ee7178ad882d2bd1b0d17c5393974b09ab027a215eba/uuid_utils-0.13.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e3909a8a1fbd79d7c8bdc874eeb83e23ccb7a7cb0aa821a49596cc96c0cce84b", size = 298273, upload-time = "2026-01-08T15:48:02.063Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/14/7061b868a8a6799c8df83768a23f313d4e22075069f01ee3c28fa82aa2c6/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:5dc4c9f749bd2511b8dcbf0891e658d7d86880022963db050722ad7b502b5e22", size = 333618, upload-time = "2026-01-08T15:48:03.503Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/f1/f48c3c9c343c9071ade5f355403e344d817412d9cf379a2d04b181282e74/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_armv7l.whl", hash = "sha256:516adf07f5b2cdb88d50f489c702b5f1a75ae8b2639bfd254f4192d5f7ee261f", size = 339104, upload-time = "2026-01-08T15:48:05.02Z" },
+ { url = "https://files.pythonhosted.org/packages/47/22/8e3142b4baffee77ce533fe956446d3699ec42f1d5252911208cbef4501e/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_i686.whl", hash = "sha256:aeee3bd89e8de6184a3ab778ce19f5ce9ad32849d1be549516e0ddb257562d8d", size = 359503, upload-time = "2026-01-08T15:48:06.347Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/1a/756f1f9e31b15019c87cd2becb1c596351c50967cd143443da38df8818d1/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_ppc64le.whl", hash = "sha256:97985256c2e59b7caa51f5c8515f64d777328562a9c900ec65e9d627baf72737", size = 467480, upload-time = "2026-01-08T15:48:07.681Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/20/a6929e98d9a461ca49e96194a82a1cc3fd5420f3a2f53cbb34fca438549e/uuid_utils-0.13.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:b7ccaa20e24c5f60f41a69ef571ed820737f9b0ade4cbeef56aaa8f80f5aa475", size = 333610, upload-time = "2026-01-08T15:48:09.375Z" },
+]
+
+[[package]]
+name = "watchdog"
+version = "6.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" },
+ { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" },
+ { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" },
+ { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" },
+ { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
+ { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
+ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
+ { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
+ { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
+ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
+ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
+]
+
+[[package]]
+name = "zstandard"
+version = "0.25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" },
+ { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" },
+ { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" },
+ { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" },
+ { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" },
+ { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" },
+ { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" },
+ { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" },
+ { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" },
+ { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" },
+ { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" },
+ { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" },
+ { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" },
+ { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" },
+ { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" },
+ { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" },
+ { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" },
+ { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" },
+ { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" },
+ { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" },
+ { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" },
+ { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" },
+ { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" },
+ { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" },
+ { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" },
+ { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" },
+]
diff --git a/langgraph/source/libs/cli/LICENSE b/langgraph/source/libs/cli/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..fc0602feecdd6748623c852ab534e1ca612673c7
--- /dev/null
+++ b/langgraph/source/libs/cli/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 LangChain, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/langgraph/source/libs/cli/Makefile b/langgraph/source/libs/cli/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..60a781d8ee48ad7a3cbbd039650a8eb8b8c35a47
--- /dev/null
+++ b/langgraph/source/libs/cli/Makefile
@@ -0,0 +1,40 @@
+.PHONY: test lint format test-integration update-schema bump-version
+
+######################
+# TESTING AND COVERAGE
+######################
+
+TEST?= "tests/unit_tests"
+test:
+ uv run pytest $(TEST)
+test-integration:
+ uv run pytest tests/integration_tests
+
+######################
+# LINTING AND FORMATTING
+######################
+
+# Define a variable for Python and notebook files.
+PYTHON_FILES=.
+MYPY_CACHE=.mypy_cache
+lint format: PYTHON_FILES=.
+lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --relative --diff-filter=d main . | grep -E '\.py$$|\.ipynb$$')
+lint_package: PYTHON_FILES=langgraph_cli
+lint_tests: PYTHON_FILES=tests
+lint_tests: MYPY_CACHE=.mypy_cache_test
+
+lint lint_diff lint_package lint_tests:
+ uv run ruff check .
+ [ "$(PYTHON_FILES)" = "" ] || uv run ruff format $(PYTHON_FILES) --diff
+ [ "$(PYTHON_FILES)" = "" ] || uv run ruff check --select I $(PYTHON_FILES)
+ [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || uv run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
+
+format format_diff:
+ uv run ruff format $(PYTHON_FILES)
+ uv run ruff check --select I --fix $(PYTHON_FILES)
+
+update-schema:
+ uv run python generate_schema.py
+
+bump-version:
+ uv run hatch version patch
diff --git a/langgraph/source/libs/cli/README.md b/langgraph/source/libs/cli/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..d3b130e86e4ca4c355e7f31445ae3d4f948d5664
--- /dev/null
+++ b/langgraph/source/libs/cli/README.md
@@ -0,0 +1,105 @@
+# LangGraph CLI
+
+The official command-line interface for LangGraph, providing tools to create, develop, and deploy LangGraph applications.
+
+## Installation
+
+Install via pip:
+```bash
+pip install langgraph-cli
+```
+
+For development mode with hot reloading:
+```bash
+pip install "langgraph-cli[inmem]"
+```
+
+## Commands
+
+### `langgraph new` 🌱
+Create a new LangGraph project from a template
+```bash
+langgraph new [PATH] --template TEMPLATE_NAME
+```
+
+### `langgraph dev` 🏃♀️
+Run LangGraph API server in development mode with hot reloading
+```bash
+langgraph dev [OPTIONS]
+ --host TEXT Host to bind to (default: 127.0.0.1)
+ --port INTEGER Port to bind to (default: 2024)
+ --no-reload Disable auto-reload
+ --debug-port INTEGER Enable remote debugging
+ --no-browser Skip opening browser window
+ -c, --config FILE Config file path (default: langgraph.json)
+```
+
+### `langgraph up` 🚀
+Launch LangGraph API server in Docker
+```bash
+langgraph up [OPTIONS]
+ -p, --port INTEGER Port to expose (default: 8123)
+ --wait Wait for services to start
+ --watch Restart on file changes
+ --verbose Show detailed logs
+ -c, --config FILE Config file path
+ -d, --docker-compose Additional services file
+```
+
+### `langgraph build`
+Build a Docker image for your LangGraph application
+```bash
+langgraph build -t IMAGE_TAG [OPTIONS]
+ --platform TEXT Target platforms (e.g., linux/amd64,linux/arm64)
+ --pull / --no-pull Use latest/local base image
+ -c, --config FILE Config file path
+```
+
+### `langgraph dockerfile`
+Generate a Dockerfile for custom deployments
+```bash
+langgraph dockerfile SAVE_PATH [OPTIONS]
+ -c, --config FILE Config file path
+```
+
+## Configuration
+
+The CLI uses a `langgraph.json` configuration file with these key settings:
+
+```json
+{
+ "dependencies": ["langchain_openai", "./your_package"], // Required: Package dependencies
+ "graphs": {
+ "my_graph": "./your_package/file.py:graph" // Required: Graph definitions
+ },
+ "env": "./.env", // Optional: Environment variables
+ "python_version": "3.11", // Optional: Python version (3.11/3.12)
+ "pip_config_file": "./pip.conf", // Optional: pip configuration
+ "dockerfile_lines": [] // Optional: Additional Dockerfile commands
+}
+```
+
+See the [full documentation](https://langchain-ai.github.io/langgraph/cloud/reference/cli/) for detailed configuration options.
+
+## Development
+
+To develop the CLI itself:
+
+1. Clone the repository
+2. Navigate to the CLI directory: `cd libs/cli`
+3. Install development dependencies: `uv pip install`
+4. Make your changes to the CLI code
+5. Test your changes:
+ ```bash
+ # Run CLI commands directly
+ uv run langgraph --help
+
+ # Or use the examples
+ cd examples
+ uv pip install
+ uv run langgraph dev # or other commands
+ ```
+
+## License
+
+This project is licensed under the terms specified in the repository's LICENSE file.
diff --git a/langgraph/source/libs/cli/__init__.py b/langgraph/source/libs/cli/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/cli/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/cli/examples/.env.example b/langgraph/source/libs/cli/examples/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..d9649108e12bbf8f931c81ce8a0ac4f402574abe
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/.env.example
@@ -0,0 +1,3 @@
+OPENAI_API_KEY=placeholder
+ANTHROPIC_API_KEY=placeholder
+TAVILY_API_KEY=placeholder
diff --git a/langgraph/source/libs/cli/examples/Makefile b/langgraph/source/libs/cli/examples/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..a2b91ff9a3a9b0c18d74ad9b3becf78f54e366fe
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/Makefile
@@ -0,0 +1,13 @@
+.PHONY: run_w_override
+
+run:
+ uv run langgraph up --watch --no-pull
+
+run_faux:
+ cd graphs && uv run langgraph up --no-pull
+
+run_graphs_reqs_a:
+ cd graphs_reqs_a && uv run langgraph up --no-pull
+
+run_graphs_reqs_b:
+ cd graphs_reqs_b && uv run langgraph up --no-pull
diff --git a/langgraph/source/libs/cli/examples/__init__.py b/langgraph/source/libs/cli/examples/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/cli/examples/graph_prerelease_reqs/__init__.py b/langgraph/source/libs/cli/examples/graph_prerelease_reqs/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graph_prerelease_reqs/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/cli/examples/graph_prerelease_reqs/agent.py b/langgraph/source/libs/cli/examples/graph_prerelease_reqs/agent.py
new file mode 100644
index 0000000000000000000000000000000000000000..57d8543a2ecb2fd827a3cea72fb3bce6ded5ce54
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graph_prerelease_reqs/agent.py
@@ -0,0 +1,88 @@
+from collections.abc import Sequence
+from typing import Annotated, Literal, TypedDict
+
+from langchain_core.messages import BaseMessage
+from langchain_openai import ChatOpenAI
+from langgraph.graph import END, StateGraph, add_messages
+from langgraph.prebuilt import ToolNode
+
+tools = []
+
+model_oai = ChatOpenAI(temperature=0)
+
+model_oai = model_oai.bind_tools(tools)
+
+
+class AgentState(TypedDict):
+ messages: Annotated[Sequence[BaseMessage], add_messages]
+
+
+# Define the function that determines whether to continue or not
+def should_continue(state):
+ messages = state["messages"]
+ last_message = messages[-1]
+ # If there are no tool calls, then we finish
+ if not last_message.tool_calls:
+ return "end"
+ # Otherwise if there is, we continue
+ else:
+ return "continue"
+
+
+# Define the function that calls the model
+def call_model(state, config):
+ model = model_oai
+ messages = state["messages"]
+ response = model.invoke(messages)
+ # We return a list, because this will get added to the existing list
+ return {"messages": [response]}
+
+
+# Define the function to execute tools
+tool_node = ToolNode(tools)
+
+
+class ContextSchema(TypedDict):
+ model: Literal["anthropic", "openai"]
+
+
+# Define a new graph
+workflow = StateGraph(AgentState, context_schema=ContextSchema)
+
+# Define the two nodes we will cycle between
+workflow.add_node("agent", call_model)
+workflow.add_node("action", tool_node)
+
+# Set the entrypoint as `agent`
+# This means that this node is the first one called
+workflow.set_entry_point("agent")
+
+# We now add a conditional edge
+workflow.add_conditional_edges(
+ # First, we define the start node. We use `agent`.
+ # This means these are the edges taken after the `agent` node is called.
+ "agent",
+ # Next, we pass in the function that will determine which node is called next.
+ should_continue,
+ # Finally we pass in a mapping.
+ # The keys are strings, and the values are other nodes.
+ # END is a special node marking that the graph should finish.
+ # What will happen is we will call `should_continue`, and then the output of that
+ # will be matched against the keys in this mapping.
+ # Based on which one it matches, that node will then be called.
+ {
+ # If `tools`, then we call the tool node.
+ "continue": "action",
+ # Otherwise we finish.
+ "end": END,
+ },
+)
+
+# We now add a normal edge from `tools` to `agent`.
+# This means that after `tools` is called, `agent` node is called next.
+workflow.add_edge("action", "agent")
+
+# Finally, we compile it!
+# This compiles it into a LangChain Runnable,
+# meaning you can use it as you would any other runnable
+graph = workflow.compile()
diff --git a/langgraph/source/libs/cli/examples/graph_prerelease_reqs/deps/additional_deps/pyproject.toml b/langgraph/source/libs/cli/examples/graph_prerelease_reqs/deps/additional_deps/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..c68fdf5617e34d557bbe7df4a8b05a398c9f960a
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graph_prerelease_reqs/deps/additional_deps/pyproject.toml
@@ -0,0 +1,9 @@
+[project]
+name = "graph-prerelease-reqs-additional-deps"
+version = "0.1.0"
+description = "Test for prerelease stuff"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+ "langgraph==1.0.2"
+]
\ No newline at end of file
diff --git a/langgraph/source/libs/cli/examples/graph_prerelease_reqs/deps/zuper_deps/pyproject.toml b/langgraph/source/libs/cli/examples/graph_prerelease_reqs/deps/zuper_deps/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..f6c1289a78f2c69ff3e842cb14abbb9e81eda065
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graph_prerelease_reqs/deps/zuper_deps/pyproject.toml
@@ -0,0 +1,9 @@
+[project]
+name = "graph-prerelease-reqs-zuper-deps"
+version = "0.1.0"
+description = "Test for prerelease stuff"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+ "langchain-openai==1.0.1"
+]
\ No newline at end of file
diff --git a/langgraph/source/libs/cli/examples/graph_prerelease_reqs/langgraph.json b/langgraph/source/libs/cli/examples/graph_prerelease_reqs/langgraph.json
new file mode 100644
index 0000000000000000000000000000000000000000..6d3a7b92b8160d69fe7b80249fd733c73212810f
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graph_prerelease_reqs/langgraph.json
@@ -0,0 +1,13 @@
+{
+ "python_version": "3.12",
+ "dependencies": [
+ ".",
+ "./deps/additional_deps",
+ "./deps/zuper_deps"
+ ],
+ "graphs": {
+ "agent": "./agent.py:graph"
+ },
+ "env": "../.env"
+ }
+
\ No newline at end of file
diff --git a/langgraph/source/libs/cli/examples/graph_prerelease_reqs/pyproject.toml b/langgraph/source/libs/cli/examples/graph_prerelease_reqs/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..b0c1ab59a20d890ff9fa3ba4e33cf3687ffec122
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graph_prerelease_reqs/pyproject.toml
@@ -0,0 +1,14 @@
+[project]
+name = "graph-prerelease-reqs"
+version = "0.1.0"
+description = "Test for prerelease stuff"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+ "langchain-openai==1.0.0a2",
+ "langchain-anthropic==1.0.0a5",
+ "langgraph==1.0.2"
+]
+
+[tool.uv]
+prerelease = "allow"
diff --git a/langgraph/source/libs/cli/examples/graph_prerelease_reqs_fail/__init__.py b/langgraph/source/libs/cli/examples/graph_prerelease_reqs_fail/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graph_prerelease_reqs_fail/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/cli/examples/graph_prerelease_reqs_fail/agent.py b/langgraph/source/libs/cli/examples/graph_prerelease_reqs_fail/agent.py
new file mode 100644
index 0000000000000000000000000000000000000000..4801c7c0bca8e486f47784cd92ce5c65db9e53df
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graph_prerelease_reqs_fail/agent.py
@@ -0,0 +1,89 @@
+from collections.abc import Sequence
+from typing import Annotated, Literal, TypedDict
+
+from langchain_community.tools.tavily_search import TavilySearchResults
+from langchain_core.messages import BaseMessage
+from langchain_openai import ChatOpenAI
+from langgraph.graph import END, StateGraph, add_messages
+from langgraph.prebuilt import ToolNode
+
+tools = [TavilySearchResults(max_results=1)]
+
+model_oai = ChatOpenAI(temperature=0)
+
+model_oai = model_oai.bind_tools(tools)
+
+
+class AgentState(TypedDict):
+ messages: Annotated[Sequence[BaseMessage], add_messages]
+
+
+# Define the function that determines whether to continue or not
+def should_continue(state):
+ messages = state["messages"]
+ last_message = messages[-1]
+ # If there are no tool calls, then we finish
+ if not last_message.tool_calls:
+ return "end"
+ # Otherwise if there is, we continue
+ else:
+ return "continue"
+
+
+# Define the function that calls the model
+def call_model(state, config):
+ model = model_oai
+ messages = state["messages"]
+ response = model.invoke(messages)
+ # We return a list, because this will get added to the existing list
+ return {"messages": [response]}
+
+
+# Define the function to execute tools
+tool_node = ToolNode(tools)
+
+
+class ContextSchema(TypedDict):
+ model: Literal["anthropic", "openai"]
+
+
+# Define a new graph
+workflow = StateGraph(AgentState, context_schema=ContextSchema)
+
+# Define the two nodes we will cycle between
+workflow.add_node("agent", call_model)
+workflow.add_node("action", tool_node)
+
+# Set the entrypoint as `agent`
+# This means that this node is the first one called
+workflow.set_entry_point("agent")
+
+# We now add a conditional edge
+workflow.add_conditional_edges(
+ # First, we define the start node. We use `agent`.
+ # This means these are the edges taken after the `agent` node is called.
+ "agent",
+ # Next, we pass in the function that will determine which node is called next.
+ should_continue,
+ # Finally we pass in a mapping.
+ # The keys are strings, and the values are other nodes.
+ # END is a special node marking that the graph should finish.
+ # What will happen is we will call `should_continue`, and then the output of that
+ # will be matched against the keys in this mapping.
+ # Based on which one it matches, that node will then be called.
+ {
+ # If `tools`, then we call the tool node.
+ "continue": "action",
+ # Otherwise we finish.
+ "end": END,
+ },
+)
+
+# We now add a normal edge from `tools` to `agent`.
+# This means that after `tools` is called, `agent` node is called next.
+workflow.add_edge("action", "agent")
+
+# Finally, we compile it!
+# This compiles it into a LangChain Runnable,
+# meaning you can use it as you would any other runnable
+graph = workflow.compile()
diff --git a/langgraph/source/libs/cli/examples/graph_prerelease_reqs_fail/langgraph.json b/langgraph/source/libs/cli/examples/graph_prerelease_reqs_fail/langgraph.json
new file mode 100644
index 0000000000000000000000000000000000000000..8bc7d8b2cde6ad730e7edda5fc6631d033ba9669
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graph_prerelease_reqs_fail/langgraph.json
@@ -0,0 +1,11 @@
+{
+ "python_version": "3.12",
+ "dependencies": [
+ "."
+ ],
+ "graphs": {
+ "agent": "./agent.py:graph"
+ },
+ "env": "../.env"
+ }
+
\ No newline at end of file
diff --git a/langgraph/source/libs/cli/examples/graph_prerelease_reqs_fail/pyproject.toml b/langgraph/source/libs/cli/examples/graph_prerelease_reqs_fail/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..27933649bfd46699261f83bad93f6d9e0e12b4fe
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graph_prerelease_reqs_fail/pyproject.toml
@@ -0,0 +1,11 @@
+[project]
+name = "graph-prerelease-reqs"
+version = "0.1.0"
+description = "Test for prerelease stuff"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+ "langchain-openai==1.0.0a2",
+ "langgraph==1.0.0a2",
+ "langchain_community>=0.3.0",
+]
\ No newline at end of file
diff --git a/langgraph/source/libs/cli/examples/graphs/__init__.py b/langgraph/source/libs/cli/examples/graphs/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graphs/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/cli/examples/graphs/agent.py b/langgraph/source/libs/cli/examples/graphs/agent.py
new file mode 100644
index 0000000000000000000000000000000000000000..931982061ac3a1502dde4239309062f7f0fd6c58
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graphs/agent.py
@@ -0,0 +1,96 @@
+from collections.abc import Sequence
+from typing import Annotated, Literal, TypedDict
+
+from langchain_anthropic import ChatAnthropic
+from langchain_community.tools.tavily_search import TavilySearchResults
+from langchain_core.messages import BaseMessage
+from langchain_openai import ChatOpenAI
+from langgraph.graph import END, StateGraph, add_messages
+from langgraph.prebuilt import ToolNode
+from langgraph.runtime import Runtime
+
+tools = [TavilySearchResults(max_results=1)]
+
+model_anth = ChatAnthropic(temperature=0, model_name="claude-3-sonnet-20240229")
+model_oai = ChatOpenAI(temperature=0)
+
+model_anth = model_anth.bind_tools(tools)
+model_oai = model_oai.bind_tools(tools)
+
+
+class AgentContext(TypedDict):
+ model: Literal["anthropic", "openai"]
+
+
+class AgentState(TypedDict):
+ messages: Annotated[Sequence[BaseMessage], add_messages]
+
+
+# Define the function that determines whether to continue or not
+def should_continue(state):
+ messages = state["messages"]
+ last_message = messages[-1]
+ # If there are no tool calls, then we finish
+ if not last_message.tool_calls:
+ return "end"
+ # Otherwise if there is, we continue
+ else:
+ return "continue"
+
+
+# Define the function that calls the model
+def call_model(state, runtime: Runtime[AgentContext]):
+ if runtime.context.get("model", "anthropic") == "anthropic":
+ model = model_anth
+ else:
+ model = model_oai
+ messages = state["messages"]
+ response = model.invoke(messages)
+ # We return a list, because this will get added to the existing list
+ return {"messages": [response]}
+
+
+# Define the function to execute tools
+tool_node = ToolNode(tools)
+
+
+# Define a new graph
+workflow = StateGraph(AgentState, context_schema=AgentContext)
+
+# Define the two nodes we will cycle between
+workflow.add_node("agent", call_model)
+workflow.add_node("action", tool_node)
+
+# Set the entrypoint as `agent`
+# This means that this node is the first one called
+workflow.set_entry_point("agent")
+
+# We now add a conditional edge
+workflow.add_conditional_edges(
+ # First, we define the start node. We use `agent`.
+ # This means these are the edges taken after the `agent` node is called.
+ "agent",
+ # Next, we pass in the function that will determine which node is called next.
+ should_continue,
+ # Finally we pass in a mapping.
+ # The keys are strings, and the values are other nodes.
+ # END is a special node marking that the graph should finish.
+ # What will happen is we will call `should_continue`, and then the output of that
+ # will be matched against the keys in this mapping.
+ # Based on which one it matches, that node will then be called.
+ {
+ # If `tools`, then we call the tool node.
+ "continue": "action",
+ # Otherwise we finish.
+ "end": END,
+ },
+)
+
+# We now add a normal edge from `tools` to `agent`.
+# This means that after `tools` is called, `agent` node is called next.
+workflow.add_edge("action", "agent")
+
+# Finally, we compile it!
+# This compiles it into a LangChain Runnable,
+# meaning you can use it as you would any other runnable
+graph = workflow.compile()
diff --git a/langgraph/source/libs/cli/examples/graphs/langgraph.json b/langgraph/source/libs/cli/examples/graphs/langgraph.json
new file mode 100644
index 0000000000000000000000000000000000000000..fce75dd9e187e476ca445035866aa5893a6e19f1
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graphs/langgraph.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://langgra.ph/schema.json",
+ "python_version": "3.12",
+ "dependencies": [
+ "langchain_community",
+ "langchain_anthropic",
+ "langchain_openai",
+ "wikipedia",
+ "scikit-learn",
+ "."
+ ],
+ "graphs": {
+ "agent": "./agent.py:graph",
+ "storm": "./storm.py:graph"
+ },
+ "env": "../.env"
+}
diff --git a/langgraph/source/libs/cli/examples/graphs/storm.py b/langgraph/source/libs/cli/examples/graphs/storm.py
new file mode 100644
index 0000000000000000000000000000000000000000..f1ab7353a94102f97544039bec20bb586d6b71bd
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graphs/storm.py
@@ -0,0 +1,636 @@
+import asyncio
+import json
+from typing import Annotated
+
+from langchain_community.retrievers import WikipediaRetriever
+from langchain_community.tools.tavily_search import TavilySearchResults
+from langchain_community.vectorstores import SKLearnVectorStore
+from langchain_core.documents import Document
+from langchain_core.messages import (
+ AIMessage,
+ AnyMessage,
+ HumanMessage,
+ ToolMessage,
+)
+from langchain_core.output_parsers import StrOutputParser
+from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
+from langchain_core.runnables import RunnableConfig, RunnableLambda
+from langchain_core.runnables import chain as as_runnable
+from langchain_core.tools import tool
+from langchain_openai import ChatOpenAI, OpenAIEmbeddings
+from langgraph.graph import END, StateGraph
+from pydantic import BaseModel, Field
+from typing_extensions import TypedDict
+
+fast_llm = ChatOpenAI(model="gpt-4o-mini")
+# Uncomment for a Fireworks model
+# fast_llm = ChatFireworks(model="accounts/fireworks/models/firefunction-v1", max_tokens=32_000)
+long_context_llm = ChatOpenAI(model="gpt-4o")
+
+
+direct_gen_outline_prompt = ChatPromptTemplate.from_messages(
+ [
+ (
+ "system",
+ "You are a Wikipedia writer. Write an outline for a Wikipedia page about a user-provided topic. Be comprehensive and specific.",
+ ),
+ ("user", "{topic}"),
+ ]
+)
+
+
+class Subsection(BaseModel):
+ subsection_title: str = Field(..., title="Title of the subsection")
+ description: str = Field(..., title="Content of the subsection")
+
+ @property
+ def as_str(self) -> str:
+ return f"### {self.subsection_title}\n\n{self.description}".strip()
+
+
+class Section(BaseModel):
+ section_title: str = Field(..., title="Title of the section")
+ description: str = Field(..., title="Content of the section")
+ subsections: list[Subsection] | None = Field(
+ default=None,
+ title="Titles and descriptions for each subsection of the Wikipedia page.",
+ )
+
+ @property
+ def as_str(self) -> str:
+ subsections = "\n\n".join(
+ f"### {subsection.subsection_title}\n\n{subsection.description}"
+ for subsection in self.subsections or []
+ )
+ return f"## {self.section_title}\n\n{self.description}\n\n{subsections}".strip()
+
+
+class Outline(BaseModel):
+ page_title: str = Field(..., title="Title of the Wikipedia page")
+ sections: list[Section] = Field(
+ default_factory=list,
+ title="Titles and descriptions for each section of the Wikipedia page.",
+ )
+
+ @property
+ def as_str(self) -> str:
+ sections = "\n\n".join(section.as_str for section in self.sections)
+ return f"# {self.page_title}\n\n{sections}".strip()
+
+
+generate_outline_direct = direct_gen_outline_prompt | fast_llm.with_structured_output(
+ Outline
+)
+
+gen_related_topics_prompt = ChatPromptTemplate.from_template(
+ """I'm writing a Wikipedia page for a topic mentioned below. Please identify and recommend some Wikipedia pages on closely related subjects. I'm looking for examples that provide insights into interesting aspects commonly associated with this topic, or examples that help me understand the typical content and structure included in Wikipedia pages for similar topics.
+
+Please list the as many subjects and urls as you can.
+
+Topic of interest: {topic}
+"""
+)
+
+
+class RelatedSubjects(BaseModel):
+ topics: list[str] = Field(
+ description="Comprehensive list of related subjects as background research.",
+ )
+
+
+expand_chain = gen_related_topics_prompt | fast_llm.with_structured_output(
+ RelatedSubjects
+)
+
+
+class Editor(BaseModel):
+ affiliation: str = Field(
+ description="Primary affiliation of the editor.",
+ )
+ name: str = Field(
+ description="Name of the editor.",
+ )
+ role: str = Field(
+ description="Role of the editor in the context of the topic.",
+ )
+ description: str = Field(
+ description="Description of the editor's focus, concerns, and motives.",
+ )
+
+ @property
+ def persona(self) -> str:
+ return f"Name: {self.name}\nRole: {self.role}\nAffiliation: {self.affiliation}\nDescription: {self.description}\n"
+
+
+class Perspectives(BaseModel):
+ editors: list[Editor] = Field(
+ description="Comprehensive list of editors with their roles and affiliations.",
+ # Add a pydantic validation/restriction to be at most M editors
+ )
+
+
+gen_perspectives_prompt = ChatPromptTemplate.from_messages(
+ [
+ (
+ "system",
+ """You need to select a diverse (and distinct) group of Wikipedia editors who will work together to create a comprehensive article on the topic. Each of them represents a different perspective, role, or affiliation related to this topic.\
+ You can use other Wikipedia pages of related topics for inspiration. For each editor, add a description of what they will focus on.
+
+ Wiki page outlines of related topics for inspiration:
+ {examples}""",
+ ),
+ ("user", "Topic of interest: {topic}"),
+ ]
+)
+
+gen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(
+ model="gpt-4o-mini"
+).with_structured_output(Perspectives)
+
+
+wikipedia_retriever = WikipediaRetriever(load_all_available_meta=True, top_k_results=1)
+
+
+def format_doc(doc, max_length=1000):
+ related = "- ".join(doc.metadata["categories"])
+ return f"### {doc.metadata['title']}\n\nSummary: {doc.page_content}\n\nRelated\n{related}"[
+ :max_length
+ ]
+
+
+def format_docs(docs):
+ return "\n\n".join(format_doc(doc) for doc in docs)
+
+
+@as_runnable
+async def survey_subjects(topic: str):
+ related_subjects = await expand_chain.ainvoke({"topic": topic})
+ retrieved_docs = await wikipedia_retriever.abatch(
+ related_subjects.topics, return_exceptions=True
+ )
+ all_docs = []
+ for docs in retrieved_docs:
+ if isinstance(docs, BaseException):
+ continue
+ all_docs.extend(docs)
+ formatted = format_docs(all_docs)
+ return await gen_perspectives_chain.ainvoke({"examples": formatted, "topic": topic})
+
+
+def add_messages(left, right):
+ if not isinstance(left, list):
+ left = [left]
+ if not isinstance(right, list):
+ right = [right]
+ return left + right
+
+
+def update_references(references, new_references):
+ if not references:
+ references = {}
+ references.update(new_references)
+ return references
+
+
+def update_editor(editor, new_editor):
+ # Can only set at the outset
+ if not editor:
+ return new_editor
+ return editor
+
+
+class InterviewState(TypedDict):
+ messages: Annotated[list[AnyMessage], add_messages]
+ references: Annotated[dict | None, update_references]
+ editor: Annotated[Editor | None, update_editor]
+
+
+gen_qn_prompt = ChatPromptTemplate.from_messages(
+ [
+ (
+ "system",
+ """You are an experienced Wikipedia writer and want to edit a specific page. \
+Besides your identity as a Wikipedia writer, you have a specific focus when researching the topic. \
+Now, you are chatting with an expert to get information. Ask good questions to get more useful information.
+
+When you have no more questions to ask, say "Thank you so much for your help!" to end the conversation.\
+Please only ask one question at a time and don't ask what you have asked before.\
+Your questions should be related to the topic you want to write.
+Be comprehensive and curious, gaining as much unique insight from the expert as possible.\
+
+Stay true to your specific perspective:
+
+{persona}""",
+ ),
+ MessagesPlaceholder(variable_name="messages", optional=True),
+ ]
+)
+
+
+def tag_with_name(ai_message: AIMessage, name: str):
+ ai_message.name = name.replace(" ", "_").replace(".", "_")
+ return ai_message
+
+
+def swap_roles(state: InterviewState, name: str):
+ converted = []
+ for message in state["messages"]:
+ if isinstance(message, AIMessage) and message.name != name:
+ message = HumanMessage(**message.dict(exclude={"type"}))
+ converted.append(message)
+ return {"messages": converted}
+
+
+@as_runnable
+async def generate_question(state: InterviewState):
+ editor = state["editor"]
+ gn_chain = (
+ RunnableLambda(swap_roles).bind(name=editor.name)
+ | gen_qn_prompt.partial(persona=editor.persona)
+ | fast_llm
+ | RunnableLambda(tag_with_name).bind(name=editor.name)
+ )
+ result = await gn_chain.ainvoke(state)
+ return {"messages": [result]}
+
+
+class Queries(BaseModel):
+ queries: list[str] = Field(
+ description="Comprehensive list of search engine queries to answer the user's questions.",
+ )
+
+
+gen_queries_prompt = ChatPromptTemplate.from_messages(
+ [
+ (
+ "system",
+ "You are a helpful research assistant. Query the search engine to answer the user's questions.",
+ ),
+ MessagesPlaceholder(variable_name="messages", optional=True),
+ ]
+)
+gen_queries_chain = gen_queries_prompt | ChatOpenAI(
+ model="gpt-4o-mini"
+).with_structured_output(Queries, include_raw=True)
+
+
+class AnswerWithCitations(BaseModel):
+ answer: str = Field(
+ description="Comprehensive answer to the user's question with citations.",
+ )
+ cited_urls: list[str] = Field(
+ description="List of urls cited in the answer.",
+ )
+
+ @property
+ def as_str(self) -> str:
+ return f"{self.answer}\n\nCitations:\n\n" + "\n".join(
+ f"[{i + 1}]: {url}" for i, url in enumerate(self.cited_urls)
+ )
+
+
+gen_answer_prompt = ChatPromptTemplate.from_messages(
+ [
+ (
+ "system",
+ """You are an expert who can use information effectively. You are chatting with a Wikipedia writer who wants\
+ to write a Wikipedia page on the topic you know. You have gathered the related information and will now use the information to form a response.
+
+Make your response as informative as possible and make sure every sentence is supported by the gathered information.
+Each response must be backed up by a citation from a reliable source, formatted as a footnote, reproducing the URLS after your response.""",
+ ),
+ MessagesPlaceholder(variable_name="messages", optional=True),
+ ]
+)
+
+gen_answer_chain = gen_answer_prompt | fast_llm.with_structured_output(
+ AnswerWithCitations, include_raw=True
+).with_config(run_name="GenerateAnswer")
+
+
+# Tavily is typically a better search engine, but your free queries are limited
+tavily_search = TavilySearchResults(max_results=4)
+
+
+@tool
+async def search_engine(query: str):
+ """Search engine to the internet."""
+ results = tavily_search.invoke(query)
+ return [{"content": r["content"], "url": r["url"]} for r in results]
+
+
+async def gen_answer(
+ state: InterviewState,
+ config: RunnableConfig | None = None,
+ name: str = "Subject_Matter_Expert",
+ max_str_len: int = 15000,
+):
+ swapped_state = swap_roles(state, name) # Convert all other AI messages
+ queries = await gen_queries_chain.ainvoke(swapped_state)
+ query_results = await search_engine.abatch(
+ queries["parsed"].queries, config, return_exceptions=True
+ )
+ successful_results = [
+ res for res in query_results if not isinstance(res, Exception)
+ ]
+ all_query_results = {
+ res["url"]: res["content"] for results in successful_results for res in results
+ }
+ # We could be more precise about handling max token length if we wanted to here
+ dumped = json.dumps(all_query_results)[:max_str_len]
+ ai_message: AIMessage = queries["raw"]
+ tool_call = queries["raw"].tool_calls[0]
+ tool_id = tool_call["id"]
+ tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)
+ swapped_state["messages"].extend([ai_message, tool_message])
+ # Only update the shared state with the final answer to avoid
+ # polluting the dialogue history with intermediate messages
+ generated = await gen_answer_chain.ainvoke(swapped_state)
+ cited_urls = set(generated["parsed"].cited_urls)
+ # Save the retrieved information to a the shared state for future reference
+ cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}
+ formatted_message = AIMessage(name=name, content=generated["parsed"].as_str)
+ return {"messages": [formatted_message], "references": cited_references}
+
+
+max_num_turns = 5
+
+
+def route_messages(state: InterviewState, name: str = "Subject_Matter_Expert"):
+ messages = state["messages"]
+ num_responses = len(
+ [m for m in messages if isinstance(m, AIMessage) and m.name == name]
+ )
+ if num_responses >= max_num_turns:
+ return END
+ last_question = messages[-2]
+ if last_question.content.endswith("Thank you so much for your help!"):
+ return END
+ return "ask_question"
+
+
+builder = StateGraph(InterviewState)
+
+builder.add_node("ask_question", generate_question)
+builder.add_node("answer_question", gen_answer)
+builder.add_conditional_edges("answer_question", route_messages)
+builder.add_edge("ask_question", "answer_question")
+
+builder.set_entry_point("ask_question")
+interview_graph = builder.compile().with_config(run_name="Conduct Interviews")
+
+refine_outline_prompt = ChatPromptTemplate.from_messages(
+ [
+ (
+ "system",
+ """You are a Wikipedia writer. You have gathered information from experts and search engines. Now, you are refining the outline of the Wikipedia page. \
+You need to make sure that the outline is comprehensive and specific. \
+Topic you are writing about: {topic}
+
+Old outline:
+
+{old_outline}""",
+ ),
+ (
+ "user",
+ "Refine the outline based on your conversations with subject-matter experts:\n\nConversations:\n\n{conversations}\n\nWrite the refined Wikipedia outline:",
+ ),
+ ]
+)
+
+# Using turbo preview since the context can get quite long
+refine_outline_chain = refine_outline_prompt | long_context_llm.with_structured_output(
+ Outline
+)
+
+
+embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
+# reference_docs = [
+# Document(page_content=v, metadata={"source": k})
+# for k, v in final_state["references"].items()
+# ]
+# # This really doesn't need to be a vectorstore for this size of data.
+# # It could just be a numpy matrix. Or you could store documents
+# # across requests if you want.
+# vectorstore = SKLearnVectorStore.from_documents(
+# reference_docs,
+# embedding=embeddings,
+# )
+# retriever = vectorstore.as_retriever(k=10)
+
+vectorstore = SKLearnVectorStore(embedding=embeddings)
+retriever = vectorstore.as_retriever(k=10)
+
+
+class SubSection(BaseModel):
+ subsection_title: str = Field(..., title="Title of the subsection")
+ content: str = Field(
+ ...,
+ title="Full content of the subsection. Include [#] citations to the cited sources where relevant.",
+ )
+
+ @property
+ def as_str(self) -> str:
+ return f"### {self.subsection_title}\n\n{self.content}".strip()
+
+
+class WikiSection(BaseModel):
+ section_title: str = Field(..., title="Title of the section")
+ content: str = Field(..., title="Full content of the section")
+ subsections: list[Subsection] | None = Field(
+ default=None,
+ title="Titles and descriptions for each subsection of the Wikipedia page.",
+ )
+ citations: list[str] = Field(default_factory=list)
+
+ @property
+ def as_str(self) -> str:
+ subsections = "\n\n".join(
+ subsection.as_str for subsection in self.subsections or []
+ )
+ citations = "\n".join([f" [{i}] {cit}" for i, cit in enumerate(self.citations)])
+ return (
+ f"## {self.section_title}\n\n{self.content}\n\n{subsections}".strip()
+ + f"\n\n{citations}".strip()
+ )
+
+
+section_writer_prompt = ChatPromptTemplate.from_messages(
+ [
+ (
+ "system",
+ "You are an expert Wikipedia writer. Complete your assigned WikiSection from the following outline:\n\n"
+ "{outline}\n\nCite your sources, using the following references:\n\n\n{docs}\n",
+ ),
+ ("user", "Write the full WikiSection for the {section} section."),
+ ]
+)
+
+
+async def retrieve(inputs: dict):
+ docs = await retriever.ainvoke(inputs["topic"] + ": " + inputs["section"])
+ formatted = "\n".join(
+ [
+ f'\n{doc.page_content}\n'
+ for doc in docs
+ ]
+ )
+ return {"docs": formatted, **inputs}
+
+
+section_writer = (
+ retrieve
+ | section_writer_prompt
+ | long_context_llm.with_structured_output(WikiSection)
+)
+
+
+writer_prompt = ChatPromptTemplate.from_messages(
+ [
+ (
+ "system",
+ "You are an expert Wikipedia author. Write the complete wiki article on {topic} using the following section drafts:\n\n"
+ "{draft}\n\nStrictly follow Wikipedia format guidelines.",
+ ),
+ (
+ "user",
+ 'Write the complete Wiki article using markdown format. Organize citations using footnotes like "[1]",'
+ " avoiding duplicates in the footer. Include URLs in the footer.",
+ ),
+ ]
+)
+
+writer = writer_prompt | long_context_llm | StrOutputParser()
+
+
+class ResearchState(TypedDict):
+ topic: str
+ outline: Outline
+ editors: list[Editor]
+ interview_results: list[InterviewState]
+ # The final sections output
+ sections: list[WikiSection]
+ article: str
+
+
+async def initialize_research(state: ResearchState):
+ topic = state["topic"]
+ coros = (
+ generate_outline_direct.ainvoke({"topic": topic}),
+ survey_subjects.ainvoke(topic),
+ )
+ results = await asyncio.gather(*coros)
+ return {
+ **state,
+ "outline": results[0],
+ "editors": results[1].editors,
+ }
+
+
+async def conduct_interviews(state: ResearchState):
+ topic = state["topic"]
+ initial_states = [
+ {
+ "editor": editor,
+ "messages": [
+ AIMessage(
+ content=f"So you said you were writing an article on {topic}?",
+ name="Subject_Matter_Expert",
+ )
+ ],
+ }
+ for editor in state["editors"]
+ ]
+ # We call in to the sub-graph here to parallelize the interviews
+ interview_results = await interview_graph.abatch(initial_states)
+
+ return {
+ **state,
+ "interview_results": interview_results,
+ }
+
+
+def format_conversation(interview_state):
+ messages = interview_state["messages"]
+ convo = "\n".join(f"{m.name}: {m.content}" for m in messages)
+ return f"Conversation with {interview_state['editor'].name}\n\n" + convo
+
+
+async def refine_outline(state: ResearchState):
+ convos = "\n\n".join(
+ [
+ format_conversation(interview_state)
+ for interview_state in state["interview_results"]
+ ]
+ )
+
+ updated_outline = await refine_outline_chain.ainvoke(
+ {
+ "topic": state["topic"],
+ "old_outline": state["outline"].as_str,
+ "conversations": convos,
+ }
+ )
+ return {**state, "outline": updated_outline}
+
+
+async def index_references(state: ResearchState):
+ all_docs = []
+ for interview_state in state["interview_results"]:
+ reference_docs = [
+ Document(page_content=v, metadata={"source": k})
+ for k, v in interview_state["references"].items()
+ ]
+ all_docs.extend(reference_docs)
+ await vectorstore.aadd_documents(all_docs)
+ return state
+
+
+async def write_sections(state: ResearchState):
+ outline = state["outline"]
+ sections = await section_writer.abatch(
+ [
+ {
+ "outline": outline.as_str,
+ "section": section.section_title,
+ "topic": state["topic"],
+ }
+ for section in outline.sections
+ ]
+ )
+ return {
+ **state,
+ "sections": sections,
+ }
+
+
+async def write_article(state: ResearchState):
+ topic = state["topic"]
+ sections = state["sections"]
+ draft = "\n\n".join([section.as_str for section in sections])
+ article = await writer.ainvoke({"topic": topic, "draft": draft})
+ return {
+ **state,
+ "article": article,
+ }
+
+
+builder_of_storm = StateGraph(ResearchState)
+
+nodes = [
+ ("init_research", initialize_research),
+ ("conduct_interviews", conduct_interviews),
+ ("refine_outline", refine_outline),
+ ("index_references", index_references),
+ ("write_sections", write_sections),
+ ("write_article", write_article),
+]
+for i in range(len(nodes)):
+ name, node = nodes[i]
+ builder_of_storm.add_node(name, node)
+ if i > 0:
+ builder_of_storm.add_edge(nodes[i - 1][0], name)
+
+builder_of_storm.set_entry_point(nodes[0][0])
+builder_of_storm.set_finish_point(nodes[-1][0])
+graph = builder_of_storm.compile()
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_a/__init__.py b/langgraph/source/libs/cli/examples/graphs_reqs_a/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_a/graphs_submod/__init__.py b/langgraph/source/libs/cli/examples/graphs_reqs_a/graphs_submod/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_a/graphs_submod/agent.py b/langgraph/source/libs/cli/examples/graphs_reqs_a/graphs_submod/agent.py
new file mode 100644
index 0000000000000000000000000000000000000000..7971b3ce00da62717ab77993dc3f745ecf17b3a4
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graphs_reqs_a/graphs_submod/agent.py
@@ -0,0 +1,99 @@
+from collections.abc import Sequence
+from pathlib import Path
+from typing import Annotated, Literal, TypedDict
+
+from langchain_anthropic import ChatAnthropic
+from langchain_community.tools.tavily_search import TavilySearchResults
+from langchain_core.messages import BaseMessage
+from langchain_openai import ChatOpenAI
+from langgraph.graph import END, StateGraph, add_messages
+from langgraph.prebuilt import ToolNode
+from langgraph.runtime import Runtime
+
+tools = [TavilySearchResults(max_results=1)]
+
+model_anth = ChatAnthropic(temperature=0, model_name="claude-3-sonnet-20240229")
+model_oai = ChatOpenAI(temperature=0)
+
+model_anth = model_anth.bind_tools(tools)
+model_oai = model_oai.bind_tools(tools)
+
+prompt = open(Path(__file__).parent.parent / "prompt.txt").read()
+subprompt = open(Path(__file__).parent / "subprompt.txt").read()
+
+
+class AgentContext(TypedDict):
+ model: Literal["anthropic", "openai"]
+
+
+class AgentState(TypedDict):
+ messages: Annotated[Sequence[BaseMessage], add_messages]
+
+
+# Define the function that determines whether to continue or not
+def should_continue(state):
+ messages = state["messages"]
+ last_message = messages[-1]
+ # If there are no tool calls, then we finish
+ if not last_message.tool_calls:
+ return "end"
+ # Otherwise if there is, we continue
+ else:
+ return "continue"
+
+
+# Define the function that calls the model
+def call_model(state, runtime: Runtime[AgentContext]):
+ if runtime.context.get("model", "anthropic") == "anthropic":
+ model = model_anth
+ else:
+ model = model_oai
+ messages = state["messages"]
+ response = model.invoke(messages)
+ # We return a list, because this will get added to the existing list
+ return {"messages": [response]}
+
+
+# Define the function to execute tools
+tool_node = ToolNode(tools)
+
+# Define a new graph
+workflow = StateGraph(AgentState, context_schema=AgentContext)
+
+# Define the two nodes we will cycle between
+workflow.add_node("agent", call_model)
+workflow.add_node("action", tool_node)
+
+# Set the entrypoint as `agent`
+# This means that this node is the first one called
+workflow.set_entry_point("agent")
+
+# We now add a conditional edge
+workflow.add_conditional_edges(
+ # First, we define the start node. We use `agent`.
+ # This means these are the edges taken after the `agent` node is called.
+ "agent",
+ # Next, we pass in the function that will determine which node is called next.
+ should_continue,
+ # Finally we pass in a mapping.
+ # The keys are strings, and the values are other nodes.
+ # END is a special node marking that the graph should finish.
+ # What will happen is we will call `should_continue`, and then the output of that
+ # will be matched against the keys in this mapping.
+ # Based on which one it matches, that node will then be called.
+ {
+ # If `tools`, then we call the tool node.
+ "continue": "action",
+ # Otherwise we finish.
+ "end": END,
+ },
+)
+
+# We now add a normal edge from `tools` to `agent`.
+# This means that after `tools` is called, `agent` node is called next.
+workflow.add_edge("action", "agent")
+
+# Finally, we compile it!
+# This compiles it into a LangChain Runnable,
+# meaning you can use it as you would any other runnable
+graph = workflow.compile()
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_a/graphs_submod/subprompt.txt b/langgraph/source/libs/cli/examples/graphs_reqs_a/graphs_submod/subprompt.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_a/hello.py b/langgraph/source/libs/cli/examples/graphs_reqs_a/hello.py
new file mode 100644
index 0000000000000000000000000000000000000000..eedc4d11aecce0247827f057f5a375826fd89c8a
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graphs_reqs_a/hello.py
@@ -0,0 +1 @@
+from graphs_reqs_a.graphs_submod.agent import graph # noqa
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_a/langgraph.json b/langgraph/source/libs/cli/examples/graphs_reqs_a/langgraph.json
new file mode 100644
index 0000000000000000000000000000000000000000..c6215ae8499a1808f64fae0391d74e1bc79f310e
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graphs_reqs_a/langgraph.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://langgra.ph/schema.json",
+ "dependencies": [
+ "."
+ ],
+ "env": "../.env",
+ "graphs": {
+ "graph": "./hello.py:graph"
+ }
+}
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_a/prompt.txt b/langgraph/source/libs/cli/examples/graphs_reqs_a/prompt.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_a/requirements.txt b/langgraph/source/libs/cli/examples/graphs_reqs_a/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b3ca5836127e379ecd65c9fa64f6207a8f6cdea7
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graphs_reqs_a/requirements.txt
@@ -0,0 +1,4 @@
+requests
+langchain_anthropic
+langchain_openai
+langchain_community
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_b/__init__.py b/langgraph/source/libs/cli/examples/graphs_reqs_b/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graphs_reqs_b/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_b/graphs_submod/__init__.py b/langgraph/source/libs/cli/examples/graphs_reqs_b/graphs_submod/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graphs_reqs_b/graphs_submod/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_b/graphs_submod/agent.py b/langgraph/source/libs/cli/examples/graphs_reqs_b/graphs_submod/agent.py
new file mode 100644
index 0000000000000000000000000000000000000000..eb81a0a7f373ed90675017addc6878b108152c5f
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graphs_reqs_b/graphs_submod/agent.py
@@ -0,0 +1,100 @@
+from collections.abc import Sequence
+from pathlib import Path
+from typing import Annotated, Literal, TypedDict
+
+from langchain_anthropic import ChatAnthropic
+from langchain_community.tools.tavily_search import TavilySearchResults
+from langchain_core.messages import BaseMessage
+from langchain_openai import ChatOpenAI
+from langgraph.graph import END, StateGraph, add_messages
+from langgraph.prebuilt import ToolNode
+from langgraph.runtime import Runtime
+
+tools = [TavilySearchResults(max_results=1)]
+
+model_anth = ChatAnthropic(temperature=0, model_name="claude-3-sonnet-20240229")
+model_oai = ChatOpenAI(temperature=0)
+
+model_anth = model_anth.bind_tools(tools)
+model_oai = model_oai.bind_tools(tools)
+
+prompt = open(Path(__file__).parent.parent / "prompt.txt").read()
+subprompt = open(Path(__file__).parent / "subprompt.txt").read()
+
+
+class AgentContext(TypedDict):
+ model: Literal["anthropic", "openai"]
+
+
+class AgentState(TypedDict):
+ messages: Annotated[Sequence[BaseMessage], add_messages]
+
+
+# Define the function that determines whether to continue or not
+def should_continue(state):
+ messages = state["messages"]
+ last_message = messages[-1]
+ # If there are no tool calls, then we finish
+ if not last_message.tool_calls:
+ return "end"
+ # Otherwise if there is, we continue
+ else:
+ return "continue"
+
+
+# Define the function that calls the model
+def call_model(state, runtime: Runtime[AgentContext]):
+ if runtime.context.get("model", "anthropic") == "anthropic":
+ model = model_anth
+ else:
+ model = model_oai
+ messages = state["messages"]
+ response = model.invoke(messages)
+ # We return a list, because this will get added to the existing list
+ return {"messages": [response]}
+
+
+# Define the function to execute tools
+tool_node = ToolNode(tools)
+
+
+# Define a new graph
+workflow = StateGraph(AgentState, context_schema=AgentContext)
+
+# Define the two nodes we will cycle between
+workflow.add_node("agent", call_model)
+workflow.add_node("action", tool_node)
+
+# Set the entrypoint as `agent`
+# This means that this node is the first one called
+workflow.set_entry_point("agent")
+
+# We now add a conditional edge
+workflow.add_conditional_edges(
+ # First, we define the start node. We use `agent`.
+ # This means these are the edges taken after the `agent` node is called.
+ "agent",
+ # Next, we pass in the function that will determine which node is called next.
+ should_continue,
+ # Finally we pass in a mapping.
+ # The keys are strings, and the values are other nodes.
+ # END is a special node marking that the graph should finish.
+ # What will happen is we will call `should_continue`, and then the output of that
+ # will be matched against the keys in this mapping.
+ # Based on which one it matches, that node will then be called.
+ {
+ # If `tools`, then we call the tool node.
+ "continue": "action",
+ # Otherwise we finish.
+ "end": END,
+ },
+)
+
+# We now add a normal edge from `tools` to `agent`.
+# This means that after `tools` is called, `agent` node is called next.
+workflow.add_edge("action", "agent")
+
+# Finally, we compile it!
+# This compiles it into a LangChain Runnable,
+# meaning you can use it as you would any other runnable
+graph = workflow.compile()
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_b/graphs_submod/subprompt.txt b/langgraph/source/libs/cli/examples/graphs_reqs_b/graphs_submod/subprompt.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_b/hello.py b/langgraph/source/libs/cli/examples/graphs_reqs_b/hello.py
new file mode 100644
index 0000000000000000000000000000000000000000..8943551a03fa229dd512840a6ad3fa7d96de3da2
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graphs_reqs_b/hello.py
@@ -0,0 +1,4 @@
+from graphs_submod.agent import graph # noqa
+from utils.greeter import greet
+
+greet()
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_b/langgraph.json b/langgraph/source/libs/cli/examples/graphs_reqs_b/langgraph.json
new file mode 100644
index 0000000000000000000000000000000000000000..c6215ae8499a1808f64fae0391d74e1bc79f310e
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graphs_reqs_b/langgraph.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://langgra.ph/schema.json",
+ "dependencies": [
+ "."
+ ],
+ "env": "../.env",
+ "graphs": {
+ "graph": "./hello.py:graph"
+ }
+}
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_b/prompt.txt b/langgraph/source/libs/cli/examples/graphs_reqs_b/prompt.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_b/requirements.txt b/langgraph/source/libs/cli/examples/graphs_reqs_b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b3ca5836127e379ecd65c9fa64f6207a8f6cdea7
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graphs_reqs_b/requirements.txt
@@ -0,0 +1,4 @@
+requests
+langchain_anthropic
+langchain_openai
+langchain_community
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_b/utils/__init__.py b/langgraph/source/libs/cli/examples/graphs_reqs_b/utils/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/examples/graphs_reqs_b/utils/greeter.py b/langgraph/source/libs/cli/examples/graphs_reqs_b/utils/greeter.py
new file mode 100644
index 0000000000000000000000000000000000000000..ab178f3faadeb99f66b47dda1e5ef9c452756f97
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/graphs_reqs_b/utils/greeter.py
@@ -0,0 +1,2 @@
+def greet():
+ print("Hello, world!")
diff --git a/langgraph/source/libs/cli/examples/langgraph.json b/langgraph/source/libs/cli/examples/langgraph.json
new file mode 100644
index 0000000000000000000000000000000000000000..0d8563e55c1fcae6b4183574bde374c2c6e58eab
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/langgraph.json
@@ -0,0 +1,17 @@
+{
+ "pip_config_file": "./pipconf.txt",
+ "dependencies": [
+ "langchain_community",
+ "langchain_anthropic",
+ "langchain_openai",
+ "wikipedia",
+ "scikit-learn",
+ "./graphs"
+ ],
+ "keep_pkg_tools": false,
+ "graphs": {
+ "agent": "./graphs/agent.py:graph",
+ "storm": "./graphs/storm.py:graph"
+ },
+ "env": ".env"
+}
diff --git a/langgraph/source/libs/cli/examples/my_app.py b/langgraph/source/libs/cli/examples/my_app.py
new file mode 100644
index 0000000000000000000000000000000000000000..15bfa54afd3e9b13aa8d9696cf7bdc7c6bff1530
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/my_app.py
@@ -0,0 +1,62 @@
+from contextlib import asynccontextmanager
+from contextvars import ContextVar
+from typing import Any
+
+from starlette.applications import Starlette
+from starlette.middleware.base import BaseHTTPMiddleware
+from starlette.responses import JSONResponse
+from starlette.routing import Route
+
+my_context_var: ContextVar[str] = ContextVar("my_context_var", default="")
+LIFESPAN_VAL = ""
+other_context_var = ContextVar("other_context_var", default="")
+
+
+@asynccontextmanager
+async def my_lifespan(app):
+ global LIFESPAN_VAL
+ LIFESPAN_VAL = "foobar-lifespan"
+ yield
+ assert LIFESPAN_VAL == "foobar-lifespan"
+ LIFESPAN_VAL = ""
+
+
+class MyContextMiddleware(BaseHTTPMiddleware):
+ async def dispatch(self, request: Any, call_next: Any) -> Any:
+ token = my_context_var.set("Foobar")
+ try:
+ response = await call_next(request)
+ return response
+ finally:
+ my_context_var.reset(token)
+
+
+async def custom_my_route(request):
+ """A great route."""
+ assert my_context_var.get() == "Foobar"
+ assert LIFESPAN_VAL == "foobar-lifespan"
+ return JSONResponse({"foo": "bar"})
+
+
+async def runs_afakeroute(request):
+ """Another great route."""
+ assert my_context_var.get() == "Foobar"
+ assert LIFESPAN_VAL == "foobar-lifespan"
+ return JSONResponse({"foo": "afakeroute"})
+
+
+async def other_middleware(request: Any, call_next: Any) -> Any:
+ other_context_var.set("foobar")
+ response = await call_next(request)
+ other_context_var.reset()
+ return response
+
+
+app = Starlette(
+ middleware=[(MyContextMiddleware, {}, {})],
+ routes=[
+ Route("/custom/my-route", custom_my_route),
+ Route("/runs/afakeroute", runs_afakeroute),
+ ],
+ lifespan=my_lifespan,
+)
diff --git a/langgraph/source/libs/cli/examples/pipconf.txt b/langgraph/source/libs/cli/examples/pipconf.txt
new file mode 100644
index 0000000000000000000000000000000000000000..eaeb330dbda2097e66871d6da35a7d53ee84ec4b
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/pipconf.txt
@@ -0,0 +1,2 @@
+[global]
+timeout = 60
diff --git a/langgraph/source/libs/cli/examples/poetry.lock b/langgraph/source/libs/cli/examples/poetry.lock
new file mode 100644
index 0000000000000000000000000000000000000000..d14131f2784a15069d9604178db805bb753e88a4
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/poetry.lock
@@ -0,0 +1,285 @@
+# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
+
+[[package]]
+name = "anyio"
+version = "4.4.0"
+description = "High level compatibility layer for multiple asynchronous event loop implementations"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
+ {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
+]
+
+[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)", "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)"]
+
+[[package]]
+name = "certifi"
+version = "2024.7.4"
+description = "Python package for providing Mozilla's CA Bundle."
+optional = false
+python-versions = ">=3.6"
+groups = ["main"]
+files = [
+ {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
+ {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
+]
+
+[[package]]
+name = "click"
+version = "8.1.7"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+ {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["main"]
+markers = "platform_system == \"Windows\""
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.1"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+markers = "python_version < \"3.11\""
+files = [
+ {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
+ {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
+ {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+description = "A minimal low-level HTTP client."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
+ {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
+]
+
+[package.dependencies]
+certifi = "*"
+h11 = ">=0.16"
+
+[package.extras]
+asyncio = ["anyio (>=4.0,<5.0)"]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (==1.*)"]
+trio = ["trio (>=0.22.0,<1.0)"]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+description = "The next generation HTTP client."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
+ {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
+]
+
+[package.dependencies]
+anyio = "*"
+certifi = "*"
+httpcore = "==1.*"
+idna = "*"
+
+[package.extras]
+brotli = ["brotli", "brotlicffi"]
+cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (==1.*)"]
+zstd = ["zstandard (>=0.18.0)"]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.0"
+description = "Consume Server-Sent Event (SSE) messages with HTTPX."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+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"
+description = "Internationalized Domain Names in Applications (IDNA)"
+optional = false
+python-versions = ">=3.5"
+groups = ["main"]
+files = [
+ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
+ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
+]
+
+[[package]]
+name = "langgraph-cli"
+version = "0.1.52"
+description = "CLI for interacting with LangGraph API"
+optional = false
+python-versions = "^3.9.0,<4.0"
+groups = ["main"]
+files = []
+develop = true
+
+[package.dependencies]
+click = "^8.1.7"
+
+[package.source]
+type = "directory"
+url = ".."
+
+[[package]]
+name = "langgraph-sdk"
+version = "0.1.29"
+description = "SDK for interacting with LangGraph API"
+optional = false
+python-versions = "^3.9.0,<4.0"
+groups = ["main"]
+files = []
+develop = true
+
+[package.dependencies]
+httpx = ">=0.25.2"
+httpx-sse = ">=0.4.0"
+orjson = ">=3.10.1"
+
+[package.source]
+type = "directory"
+url = "../../sdk-py"
+
+[[package]]
+name = "orjson"
+version = "3.10.5"
+description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "orjson-3.10.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:545d493c1f560d5ccfc134803ceb8955a14c3fcb47bbb4b2fee0232646d0b932"},
+ {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4324929c2dd917598212bfd554757feca3e5e0fa60da08be11b4aa8b90013c1"},
+ {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c13ca5e2ddded0ce6a927ea5a9f27cae77eee4c75547b4297252cb20c4d30e6"},
+ {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6c8e30adfa52c025f042a87f450a6b9ea29649d828e0fec4858ed5e6caecf63"},
+ {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338fd4f071b242f26e9ca802f443edc588fa4ab60bfa81f38beaedf42eda226c"},
+ {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6970ed7a3126cfed873c5d21ece1cd5d6f83ca6c9afb71bbae21a0b034588d96"},
+ {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:235dadefb793ad12f7fa11e98a480db1f7c6469ff9e3da5e73c7809c700d746b"},
+ {file = "orjson-3.10.5-cp310-none-win32.whl", hash = "sha256:be79e2393679eda6a590638abda16d167754393f5d0850dcbca2d0c3735cebe2"},
+ {file = "orjson-3.10.5-cp310-none-win_amd64.whl", hash = "sha256:c4a65310ccb5c9910c47b078ba78e2787cb3878cdded1702ac3d0da71ddc5228"},
+ {file = "orjson-3.10.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cdf7365063e80899ae3a697def1277c17a7df7ccfc979990a403dfe77bb54d40"},
+ {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b68742c469745d0e6ca5724506858f75e2f1e5b59a4315861f9e2b1df77775a"},
+ {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d10cc1b594951522e35a3463da19e899abe6ca95f3c84c69e9e901e0bd93d38"},
+ {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcbe82b35d1ac43b0d84072408330fd3295c2896973112d495e7234f7e3da2e1"},
+ {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c0eb7e0c75e1e486c7563fe231b40fdd658a035ae125c6ba651ca3b07936f5"},
+ {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:53ed1c879b10de56f35daf06dbc4a0d9a5db98f6ee853c2dbd3ee9d13e6f302f"},
+ {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:099e81a5975237fda3100f918839af95f42f981447ba8f47adb7b6a3cdb078fa"},
+ {file = "orjson-3.10.5-cp311-none-win32.whl", hash = "sha256:1146bf85ea37ac421594107195db8bc77104f74bc83e8ee21a2e58596bfb2f04"},
+ {file = "orjson-3.10.5-cp311-none-win_amd64.whl", hash = "sha256:36a10f43c5f3a55c2f680efe07aa93ef4a342d2960dd2b1b7ea2dd764fe4a37c"},
+ {file = "orjson-3.10.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:68f85ecae7af14a585a563ac741b0547a3f291de81cd1e20903e79f25170458f"},
+ {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28afa96f496474ce60d3340fe8d9a263aa93ea01201cd2bad844c45cd21f5268"},
+ {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cd684927af3e11b6e754df80b9ffafd9fb6adcaa9d3e8fdd5891be5a5cad51e"},
+ {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d21b9983da032505f7050795e98b5d9eee0df903258951566ecc358f6696969"},
+ {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ad1de7fef79736dde8c3554e75361ec351158a906d747bd901a52a5c9c8d24b"},
+ {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d97531cdfe9bdd76d492e69800afd97e5930cb0da6a825646667b2c6c6c0211"},
+ {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d69858c32f09c3e1ce44b617b3ebba1aba030e777000ebdf72b0d8e365d0b2b3"},
+ {file = "orjson-3.10.5-cp312-none-win32.whl", hash = "sha256:64c9cc089f127e5875901ac05e5c25aa13cfa5dbbbd9602bda51e5c611d6e3e2"},
+ {file = "orjson-3.10.5-cp312-none-win_amd64.whl", hash = "sha256:b2efbd67feff8c1f7728937c0d7f6ca8c25ec81373dc8db4ef394c1d93d13dc5"},
+ {file = "orjson-3.10.5-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:03b565c3b93f5d6e001db48b747d31ea3819b89abf041ee10ac6988886d18e01"},
+ {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:584c902ec19ab7928fd5add1783c909094cc53f31ac7acfada817b0847975f26"},
+ {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a35455cc0b0b3a1eaf67224035f5388591ec72b9b6136d66b49a553ce9eb1e6"},
+ {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1670fe88b116c2745a3a30b0f099b699a02bb3482c2591514baf5433819e4f4d"},
+ {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185c394ef45b18b9a7d8e8f333606e2e8194a50c6e3c664215aae8cf42c5385e"},
+ {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ca0b3a94ac8d3886c9581b9f9de3ce858263865fdaa383fbc31c310b9eac07c9"},
+ {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dfc91d4720d48e2a709e9c368d5125b4b5899dced34b5400c3837dadc7d6271b"},
+ {file = "orjson-3.10.5-cp38-none-win32.whl", hash = "sha256:c05f16701ab2a4ca146d0bca950af254cb7c02f3c01fca8efbbad82d23b3d9d4"},
+ {file = "orjson-3.10.5-cp38-none-win_amd64.whl", hash = "sha256:8a11d459338f96a9aa7f232ba95679fc0c7cedbd1b990d736467894210205c09"},
+ {file = "orjson-3.10.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:85c89131d7b3218db1b24c4abecea92fd6c7f9fab87441cfc342d3acc725d807"},
+ {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66215277a230c456f9038d5e2d84778141643207f85336ef8d2a9da26bd7ca"},
+ {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51bbcdea96cdefa4a9b4461e690c75ad4e33796530d182bdd5c38980202c134a"},
+ {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbead71dbe65f959b7bd8cf91e0e11d5338033eba34c114f69078d59827ee139"},
+ {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df58d206e78c40da118a8c14fc189207fffdcb1f21b3b4c9c0c18e839b5a214"},
+ {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4057c3b511bb8aef605616bd3f1f002a697c7e4da6adf095ca5b84c0fd43595"},
+ {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b39e006b00c57125ab974362e740c14a0c6a66ff695bff44615dcf4a70ce2b86"},
+ {file = "orjson-3.10.5-cp39-none-win32.whl", hash = "sha256:eded5138cc565a9d618e111c6d5c2547bbdd951114eb822f7f6309e04db0fb47"},
+ {file = "orjson-3.10.5-cp39-none-win_amd64.whl", hash = "sha256:cc28e90a7cae7fcba2493953cff61da5a52950e78dc2dacfe931a317ee3d8de7"},
+ {file = "orjson-3.10.5.tar.gz", hash = "sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d"},
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+description = "Sniff out which async library your code is running under"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
+ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+markers = "python_version < \"3.11\""
+files = [
+ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
+ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
+]
+
+[metadata]
+lock-version = "2.1"
+python-versions = "^3.9.0,<4.0"
+content-hash = "ec5109729f30d2033a10a10e8f8d3ed94c7d96d5d31025b4815b0123664bb063"
diff --git a/langgraph/source/libs/cli/examples/pyproject.toml b/langgraph/source/libs/cli/examples/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..c8dd2d7e19ac014628ee416af7a2483abb77339a
--- /dev/null
+++ b/langgraph/source/libs/cli/examples/pyproject.toml
@@ -0,0 +1,21 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "langgraph-examples"
+version = "0.1.0"
+description = ""
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+ "langgraph-cli",
+ "langgraph-sdk",
+]
+
+[tool.uv.sources]
+langgraph-cli = { path = "../cli", editable = true }
+langgraph-sdk = { path = "../sdk_py", editable = true }
+
+[tool.hatch.build]
+packages = []
diff --git a/langgraph/source/libs/cli/generate_schema.py b/langgraph/source/libs/cli/generate_schema.py
new file mode 100644
index 0000000000000000000000000000000000000000..32543c03761d82537094d734b9eb11770ec6e1d7
--- /dev/null
+++ b/langgraph/source/libs/cli/generate_schema.py
@@ -0,0 +1,249 @@
+#!/usr/bin/env python3
+"""
+Script to generate a JSON schema for the langgraph-cli Config class.
+
+This script creates a schema.json file that can be referenced in langgraph.json files
+to provide IDE autocompletion and validation.
+"""
+
+import inspect
+import json
+import textwrap
+from pathlib import Path
+
+import msgspec
+
+from langgraph_cli.schemas import (
+ AuthConfig,
+ CacheConfig,
+ CheckpointerConfig,
+ Config,
+ ConfigurableHeaderConfig,
+ CorsConfig,
+ HttpConfig,
+ IndexConfig,
+ SecurityConfig,
+ SerdeConfig,
+ StoreConfig,
+ ThreadTTLConfig,
+ TTLConfig,
+ WebhooksConfig,
+ WebhookUrlPolicy,
+)
+
+
+def add_descriptions_to_schema(schema, cls):
+ """Add docstring descriptions to the schema properties."""
+ if schema.get("description"):
+ schema["description"] = inspect.cleandoc(schema["description"])
+ elif class_doc := inspect.getdoc(cls):
+ schema["description"] = inspect.cleandoc(class_doc)
+ # Get attribute docstrings from the class
+ attr_docs = {}
+
+ # Also check class annotations for docstrings
+ source_lines = inspect.getsourcelines(cls)[0]
+ current_attr = None
+ docstring_lines = []
+
+ for line in source_lines:
+ line = line.strip()
+
+ # Check for attribute definition (TypedDict style)
+ if ":" in line and not line.startswith("#") and not line.startswith('"""'):
+ parts = line.split(":", 1)
+ if len(parts) == 2 and parts[0].strip().isidentifier():
+ # If we were collecting a docstring, save it for the previous attribute
+ if current_attr and docstring_lines:
+ attr_docs[current_attr] = "\n".join(docstring_lines).strip('"')
+ docstring_lines = []
+
+ current_attr = parts[0].strip()
+
+ # Check for docstring after attribute
+ elif line.startswith('"""') and current_attr:
+ # Start or end of a docstring
+ if len(line) > 3 and line.endswith('"""'):
+ # Single line docstring
+ attr_docs[current_attr] = line.strip('"')
+ current_attr = None
+ elif docstring_lines:
+ # End of multi-line docstring
+ docstring_lines.append(line.rstrip('"'))
+ attr_docs[current_attr] = "\n".join(docstring_lines).strip('"')
+ docstring_lines = []
+ current_attr = None
+ else:
+ # Start of multi-line docstring
+ docstring_lines.append(line.lstrip('"'))
+
+ # Continue multi-line docstring
+ elif docstring_lines and current_attr:
+ docstring_lines.append(line.strip('"'))
+
+ # Add the last docstring if there is one
+ if current_attr and docstring_lines:
+ attr_docs[current_attr] = "\n".join(docstring_lines).strip('"')
+
+ # Add descriptions to properties
+ if "properties" in schema:
+ for prop_name, prop_schema in schema["properties"].items():
+ # First try to get from attribute docstrings
+ if prop_name in attr_docs and "description" not in prop_schema:
+ prop_schema["description"] = textwrap.dedent(attr_docs[prop_name])
+ # Fall back to class docstring parsing
+ elif class_doc:
+ for line in class_doc.split("\n"):
+ if line.strip().startswith(
+ f"{prop_name}:"
+ ) or line.strip().startswith(f'"{prop_name}"'):
+ description = line.split(":", 1)[1].strip()
+ if description and "description" not in prop_schema:
+ prop_schema["description"] = description
+ break
+
+ # Recursively process nested definitions
+ if "$defs" in schema:
+ for def_name, def_schema in schema["$defs"].items():
+ # Find the class that corresponds to this definition
+ for potential_cls in [
+ Config,
+ StoreConfig,
+ IndexConfig,
+ AuthConfig,
+ SecurityConfig,
+ HttpConfig,
+ CorsConfig,
+ CacheConfig,
+ ThreadTTLConfig,
+ CheckpointerConfig,
+ SerdeConfig,
+ TTLConfig,
+ ConfigurableHeaderConfig,
+ WebhooksConfig,
+ WebhookUrlPolicy,
+ ]:
+ if potential_cls.__name__ == def_name:
+ add_descriptions_to_schema(def_schema, potential_cls)
+ break
+
+ return schema
+
+
+def generate_schema():
+ """Generate a JSON schema for the Config class using msgspec."""
+ # Generate the basic schema
+ schema = msgspec.json.schema(Config)
+
+ # Add title and description
+ schema["title"] = "LangGraph CLI Configuration"
+ schema["description"] = "Configuration schema for langgraph-cli"
+
+ # Add docstring descriptions
+ schema = add_descriptions_to_schema(schema, Config)
+
+ # Add constraint that only one of python_version or node_version should be specified
+ config_schema = schema["$defs"]["Config"]
+
+ # Create two subschemas: one with python_version and one with node_version
+ # Define properties specific to Python projects
+ python_specific_props = ["python_version", "pip_config_file"]
+ # Define properties specific to Node.js projects
+ node_specific_props = ["node_version"]
+ # Define properties common to both project types
+ common_props = [
+ k
+ for k in config_schema["properties"]
+ if k not in python_specific_props and k not in node_specific_props
+ ]
+
+ # Create Python schema with python_version and pip_config_file
+ python_schema = {
+ "type": "object",
+ "properties": {
+ # Include Python-specific properties
+ **{k: config_schema["properties"][k].copy() for k in python_specific_props},
+ # Include common properties
+ **{k: config_schema["properties"][k].copy() for k in common_props},
+ },
+ "required": ["dependencies", "graphs"],
+ }
+
+ # Add enum constraint for python_version
+ if "python_version" in python_schema["properties"]:
+ python_schema["properties"]["python_version"]["enum"] = ["3.11", "3.12", "3.13"]
+
+ # Create Node.js schema with node_version
+ node_schema = {
+ "type": "object",
+ "properties": {
+ # Include Node-specific properties
+ **{k: config_schema["properties"][k].copy() for k in node_specific_props},
+ # Include common properties
+ **{k: config_schema["properties"][k].copy() for k in common_props},
+ },
+ "required": ["node_version", "graphs"],
+ }
+
+ # Add enum constraint for node_version
+ if "node_version" in node_schema["properties"]:
+ node_schema["properties"]["node_version"]["anyOf"] = [
+ {"type": "string", "enum": ["20"]},
+ {"type": "null"},
+ ]
+
+ # Add enum constraint for image_distro
+ if "image_distro" in node_schema["properties"]:
+ node_schema["properties"]["image_distro"]["anyOf"] = [
+ {"type": "string", "enum": ["debian", "wolfi"]},
+ {"type": "null"},
+ ]
+
+ # Replace the Config schema with a oneOf constraint
+ config_schema["oneOf"] = [python_schema, node_schema]
+
+ # Remove the properties field as it's now defined in the oneOf subschemas
+ if "properties" in config_schema:
+ del config_schema["properties"]
+
+ return schema
+
+
+def main():
+ """Generate the schema and write it to a file."""
+ schema = generate_schema()
+
+ # Add versioning to the schema
+ import importlib.metadata
+
+ try:
+ version = importlib.metadata.version("langgraph_cli").split(".")
+ schema_version = f"v{version[0]}"
+ except importlib.metadata.PackageNotFoundError:
+ schema_version = "v1"
+
+ # Add version to schema
+ schema["version"] = schema_version
+
+ config_dir = Path(__file__).parent / "schemas"
+
+ # Create versioned schema file
+ versioned_path = config_dir / f"schema.{schema_version}.json"
+ with open(versioned_path, "w") as f:
+ json.dump(schema, f, indent=2)
+
+ # Also create a latest version
+ latest_path = config_dir / "schema.json"
+ with open(latest_path, "w") as f:
+ json.dump(schema, f, indent=2)
+
+ print(f"Schema written to {versioned_path} and {latest_path}")
+ print(
+ f"You can now add '$schema: https://raw.githubusercontent.com/langchain-ai/langgraph/refs/heads/main/libs/cli/schemas/schema.json'"
+ f" or '$schema: https://raw.githubusercontent.com/langchain-ai/langgraph/refs/heads/main/libs/cli/schemas/schema.{schema_version}.json'"
+ " to your langgraph.json files"
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/langgraph/source/libs/cli/js-examples/.dockerignore b/langgraph/source/libs/cli/js-examples/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..76add878f8dd778c3381fb3da45c8140db7db510
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/.dockerignore
@@ -0,0 +1,2 @@
+node_modules
+dist
\ No newline at end of file
diff --git a/langgraph/source/libs/cli/js-examples/.editorconfig b/langgraph/source/libs/cli/js-examples/.editorconfig
new file mode 100644
index 0000000000000000000000000000000000000000..1ed453a371ba5c23ea9e365f9a6984d6aec0a13e
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/.editorconfig
@@ -0,0 +1,10 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+
+[*.{js,json,yml}]
+charset = utf-8
+indent_style = space
+indent_size = 2
diff --git a/langgraph/source/libs/cli/js-examples/.env.example b/langgraph/source/libs/cli/js-examples/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..1f381d1b129b523afeec2434dc90ad4ca76ab28e
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/.env.example
@@ -0,0 +1,3 @@
+# Copy this over:
+# cp .env.example .env
+# Then modify to suit your needs
\ No newline at end of file
diff --git a/langgraph/source/libs/cli/js-examples/.eslintrc.cjs b/langgraph/source/libs/cli/js-examples/.eslintrc.cjs
new file mode 100644
index 0000000000000000000000000000000000000000..da4c3ecb471bcd309ae38967e5e24cbb5f66ee23
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/.eslintrc.cjs
@@ -0,0 +1,62 @@
+module.exports = {
+ extends: [
+ "eslint:recommended",
+ "prettier",
+ "plugin:@typescript-eslint/recommended",
+ ],
+ parserOptions: {
+ ecmaVersion: 12,
+ parser: "@typescript-eslint/parser",
+ project: "./tsconfig.json",
+ sourceType: "module",
+ },
+ plugins: ["import", "@typescript-eslint", "no-instanceof"],
+ ignorePatterns: [
+ ".eslintrc.cjs",
+ "scripts",
+ "src/utils/lodash/*",
+ "node_modules",
+ "dist",
+ "dist-cjs",
+ "*.js",
+ "*.cjs",
+ "*.d.ts",
+ ],
+ rules: {
+ "no-process-env": 2,
+ "no-instanceof/no-instanceof": 2,
+ "@typescript-eslint/explicit-module-boundary-types": 0,
+ "@typescript-eslint/no-empty-function": 0,
+ "@typescript-eslint/no-shadow": 0,
+ "@typescript-eslint/no-empty-interface": 0,
+ "@typescript-eslint/no-use-before-define": ["error", "nofunc"],
+ "@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
+ "@typescript-eslint/no-floating-promises": "error",
+ "@typescript-eslint/no-misused-promises": "error",
+ camelcase: 0,
+ "class-methods-use-this": 0,
+ "import/extensions": [2, "ignorePackages"],
+ "import/no-extraneous-dependencies": [
+ "error",
+ { devDependencies: ["**/*.test.ts"] },
+ ],
+ "import/no-unresolved": 0,
+ "import/prefer-default-export": 0,
+ "keyword-spacing": "error",
+ "max-classes-per-file": 0,
+ "max-len": 0,
+ "no-await-in-loop": 0,
+ "no-bitwise": 0,
+ "no-console": 0,
+ "no-restricted-syntax": 0,
+ "no-shadow": 0,
+ "no-continue": 0,
+ "no-underscore-dangle": 0,
+ "no-use-before-define": 0,
+ "no-useless-constructor": 0,
+ "no-return-await": 0,
+ "consistent-return": 0,
+ "no-else-return": 0,
+ "new-cap": ["error", { properties: false, capIsNew: false }],
+ },
+};
diff --git a/langgraph/source/libs/cli/js-examples/LICENSE b/langgraph/source/libs/cli/js-examples/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..57d0481d4b50c89fee2cb9e88242639d8e674028
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 LangChain
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/langgraph/source/libs/cli/js-examples/README.md b/langgraph/source/libs/cli/js-examples/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..799fe90f1e1c598dc18b68f23da0816d5bda7032
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/README.md
@@ -0,0 +1,79 @@
+# New LangGraph.js Project
+
+[](https://github.com/langchain-ai/new-langgraphjs-project/actions/workflows/unit-tests.yml)
+[](https://github.com/langchain-ai/new-langgraphjs-project/actions/workflows/integration-tests.yml)
+[](https://langgraph-studio.vercel.app/templates/open?githubUrl=https://github.com/langchain-ai/new-langgraphjs-project)
+
+This template demonstrates a simple chatbot implemented using [LangGraph.js](https://github.com/langchain-ai/langgraphjs), designed for [LangGraph Studio](https://github.com/langchain-ai/langgraph-studio). The chatbot maintains persistent chat memory, allowing for coherent conversations across multiple interactions.
+
+
+
+The core logic, defined in `src/agent/graph.ts`, showcases a straightforward chatbot that responds to user queries while maintaining context from previous messages.
+
+## What it does
+
+The simple chatbot:
+
+1. Takes a user **message** as input
+2. Maintains a history of the conversation
+3. Returns a placeholder response, updating the conversation history
+
+This template provides a foundation that can be easily customized and extended to create more complex conversational agents.
+
+## Getting Started
+
+Assuming you have already [installed LangGraph Studio](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file#download), to set up:
+
+1. Create a `.env` file. This template does not require any environment variables by default, but you will likely want to add some when customizing.
+
+```bash
+cp .env.example .env
+```
+
+
+
+
+
+2. Open the folder in LangGraph Studio!
+3. Customize the code as needed.
+
+## How to customize
+
+1. **Add an LLM call**: You can select and install a chat model wrapper from [the LangChain.js ecosystem](https://js.langchain.com/docs/integrations/chat/), or use LangGraph.js without LangChain.js.
+2. **Extend the graph**: The core logic of the chatbot is defined in [graph.ts](./src/agent/graph.ts). You can modify this file to add new nodes, edges, or change the flow of the conversation.
+
+You can also extend this template by:
+
+- Adding [custom tools or functions](https://js.langchain.com/docs/how_to/tool_calling) to enhance the chatbot's capabilities.
+- Implementing additional logic for handling specific types of user queries or tasks.
+- Add retrieval-augmented generation (RAG) capabilities by integrating [external APIs or databases](https://langchain-ai.github.io/langgraphjs/tutorials/rag/langgraph_agentic_rag/) to provide more customized responses.
+
+## Development
+
+While iterating on your graph, you can edit past state and rerun your app from previous states to debug specific nodes. Local changes will be automatically applied via hot reload. Try experimenting with:
+
+- Modifying the system prompt to give your chatbot a unique personality.
+- Adding new nodes to the graph for more complex conversation flows.
+- Implementing conditional logic to handle different types of user inputs.
+
+Follow-up requests will be appended to the same thread. You can create an entirely new thread, clearing previous history, using the `+` button in the top right.
+
+For more advanced features and examples, refer to the [LangGraph.js documentation](https://github.com/langchain-ai/langgraphjs). These resources can help you adapt this template for your specific use case and build more sophisticated conversational agents.
+
+LangGraph Studio also integrates with [LangSmith](https://smith.langchain.com/) for more in-depth tracing and collaboration with teammates, allowing you to analyze and optimize your chatbot's performance.
+
+
diff --git a/langgraph/source/libs/cli/js-examples/jest.config.js b/langgraph/source/libs/cli/js-examples/jest.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..9e8937435158ceef918958454726118faf89f599
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/jest.config.js
@@ -0,0 +1,18 @@
+export default {
+ preset: "ts-jest/presets/default-esm",
+ moduleNameMapper: {
+ "^(\\.{1,2}/.*)\\.js$": "$1",
+ },
+ transform: {
+ "^.+\\.tsx?$": [
+ "ts-jest",
+ {
+ useESM: true,
+ },
+ ],
+ },
+ extensionsToTreatAsEsm: [".ts"],
+ setupFiles: ["dotenv/config"],
+ passWithNoTests: true,
+ testTimeout: 20_000,
+};
diff --git a/langgraph/source/libs/cli/js-examples/langgraph.json b/langgraph/source/libs/cli/js-examples/langgraph.json
new file mode 100644
index 0000000000000000000000000000000000000000..79f2b36736703c152e673a292be24896b4cc2ec2
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/langgraph.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://langgra.ph/schema.json",
+ "node_version": "20",
+ "graphs": {
+ "agent": "./src/agent/graph.ts:graph"
+ },
+ "env": ".env",
+ "dependencies": ["."]
+}
diff --git a/langgraph/source/libs/cli/js-examples/package.json b/langgraph/source/libs/cli/js-examples/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..f7b8daa03f32d29443d6c3ad76d8cf49c5d729da
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "example-graph",
+ "version": "0.0.1",
+ "description": "A starter template for creating a LangGraph workflow.",
+ "packageManager": "yarn@1.22.22",
+ "main": "my_app/graph.ts",
+ "author": "Your Name",
+ "license": "MIT",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "tsc",
+ "clean": "rm -rf dist",
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.test\\.ts$ --testPathIgnorePatterns=\\.int\\.test\\.ts$",
+ "test:int": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.int\\.test\\.ts$",
+ "format": "prettier --write .",
+ "lint": "eslint src",
+ "format:check": "prettier --check .",
+ "lint:langgraph-json": "node scripts/checkLanggraphPaths.js",
+ "lint:all": "yarn lint & yarn lint:langgraph-json & yarn format:check",
+ "test:all": "yarn test && yarn test:int && yarn lint:langgraph"
+ },
+ "dependencies": {
+ "@langchain/core": "^0.3.2",
+ "@langchain/langgraph": "^0.2.5"
+ },
+ "devDependencies": {
+ "@eslint/eslintrc": "^3.1.0",
+ "@eslint/js": "^9.9.1",
+ "@tsconfig/recommended": "^1.0.7",
+ "@types/jest": "^29.5.0",
+ "@typescript-eslint/eslint-plugin": "^5.59.8",
+ "@typescript-eslint/parser": "^5.59.8",
+ "dotenv": "^16.4.5",
+ "eslint": "^8.41.0",
+ "eslint-config-prettier": "^8.8.0",
+ "eslint-plugin-import": "^2.27.5",
+ "eslint-plugin-no-instanceof": "^1.0.1",
+ "eslint-plugin-prettier": "^4.2.1",
+ "jest": "^29.7.0",
+ "prettier": "^3.3.3",
+ "ts-jest": "^29.1.0",
+ "typescript": "^5.3.3"
+ }
+}
diff --git a/langgraph/source/libs/cli/js-examples/src/agent/graph.ts b/langgraph/source/libs/cli/js-examples/src/agent/graph.ts
new file mode 100644
index 0000000000000000000000000000000000000000..244f88ee491d4040ed9ba68524be44c453534417
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/src/agent/graph.ts
@@ -0,0 +1,104 @@
+/**
+ * Starter LangGraph.js Template
+ * Make this code your own!
+ */
+import { StateGraph } from "@langchain/langgraph";
+import { RunnableConfig } from "@langchain/core/runnables";
+import { StateAnnotation } from "./state.js";
+
+/**
+ * Define a node, these do the work of the graph and should have most of the logic.
+ * Must return a subset of the properties set in StateAnnotation.
+ * @param state The current state of the graph.
+ * @param config Extra parameters passed into the state graph.
+ * @returns Some subset of parameters of the graph state, used to update the state
+ * for the edges and nodes executed next.
+ */
+const callModel = async (
+ state: typeof StateAnnotation.State,
+ _config: RunnableConfig,
+): Promise => {
+ /**
+ * Do some work... (e.g. call an LLM)
+ * For example, with LangChain you could do something like:
+ *
+ * ```bash
+ * $ npm i @langchain/anthropic
+ * ```
+ *
+ * ```ts
+ * import { ChatAnthropic } from "@langchain/anthropic";
+ * const model = new ChatAnthropic({
+ * model: "claude-3-5-sonnet-20240620",
+ * apiKey: process.env.ANTHROPIC_API_KEY,
+ * });
+ * const res = await model.invoke(state.messages);
+ * ```
+ *
+ * Or, with an SDK directly:
+ *
+ * ```bash
+ * $ npm i openai
+ * ```
+ *
+ * ```ts
+ * import OpenAI from "openai";
+ * const openai = new OpenAI({
+ * apiKey: process.env.OPENAI_API_KEY,
+ * });
+ *
+ * const chatCompletion = await openai.chat.completions.create({
+ * messages: [{
+ * role: state.messages[0]._getType(),
+ * content: state.messages[0].content,
+ * }],
+ * model: "gpt-4o-mini",
+ * });
+ * ```
+ */
+ console.log("Current state:", state);
+ return {
+ messages: [
+ {
+ role: "assistant",
+ content: `Hi there! How are you?`,
+ },
+ ],
+ };
+};
+
+/**
+ * Routing function: Determines whether to continue research or end the builder.
+ * This function decides if the gathered information is satisfactory or if more research is needed.
+ *
+ * @param state - The current state of the research builder
+ * @returns Either "callModel" to continue research or END to finish the builder
+ */
+export const route = (
+ state: typeof StateAnnotation.State,
+): "__end__" | "callModel" => {
+ if (state.messages.length > 0) {
+ return "__end__";
+ }
+ // Loop back
+ return "callModel";
+};
+
+// Finally, create the graph itself.
+const builder = new StateGraph(StateAnnotation)
+ // Add the nodes to do the work.
+ // Chaining the nodes together in this way
+ // updates the types of the StateGraph instance
+ // so you have static type checking when it comes time
+ // to add the edges.
+ .addNode("callModel", callModel)
+ // Regular edges mean "always transition to node B after node A is done"
+ // The "__start__" and "__end__" nodes are "virtual" nodes that are always present
+ // and represent the beginning and end of the builder.
+ .addEdge("__start__", "callModel")
+ // Conditional edges optionally route to different nodes (or end)
+ .addConditionalEdges("callModel", route);
+
+export const graph = builder.compile();
+
+graph.name = "New Agent";
diff --git a/langgraph/source/libs/cli/js-examples/src/agent/state.ts b/langgraph/source/libs/cli/js-examples/src/agent/state.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6f796340516063cdb08b51c71ce03f07adf30578
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/src/agent/state.ts
@@ -0,0 +1,59 @@
+import { BaseMessage, BaseMessageLike } from "@langchain/core/messages";
+import { Annotation, messagesStateReducer } from "@langchain/langgraph";
+
+/**
+ * A graph's StateAnnotation defines three main things:
+ * 1. The structure of the data to be passed between nodes (which "channels" to read from/write to and their types)
+ * 2. Default values for each field
+ * 3. Reducers for the state's. Reducers are functions that determine how to apply updates to the state.
+ * See [Reducers](https://langchain-ai.github.io/langgraphjs/concepts/low_level/#reducers) for more information.
+ */
+
+// This is the primary state of your agent, where you can store any information
+export const StateAnnotation = Annotation.Root({
+ /**
+ * Messages track the primary execution state of the agent.
+ *
+ * Typically accumulates a pattern of:
+ *
+ * 1. HumanMessage - user input
+ * 2. AIMessage with .tool_calls - agent picking tool(s) to use to collect
+ * information
+ * 3. ToolMessage(s) - the responses (or errors) from the executed tools
+ *
+ * (... repeat steps 2 and 3 as needed ...)
+ * 4. AIMessage without .tool_calls - agent responding in unstructured
+ * format to the user.
+ *
+ * 5. HumanMessage - user responds with the next conversational turn.
+ *
+ * (... repeat steps 2-5 as needed ... )
+ *
+ * Merges two lists of messages or message-like objects with role and content,
+ * updating existing messages by ID.
+ *
+ * Message-like objects are automatically coerced by `messagesStateReducer` into
+ * LangChain message classes. If a message does not have a given id,
+ * LangGraph will automatically assign one.
+ *
+ * By default, this ensures the state is "append-only", unless the
+ * new message has the same ID as an existing message.
+ *
+ * Returns:
+ * A new list of messages with the messages from \`right\` merged into \`left\`.
+ * If a message in \`right\` has the same ID as a message in \`left\`, the
+ * message from \`right\` will replace the message from \`left\`.`
+ */
+ messages: Annotation({
+ reducer: messagesStateReducer,
+ default: () => [],
+ }),
+ /**
+ * Feel free to add additional attributes to your state as needed.
+ * Common examples include retrieved documents, extracted entities, API connections, etc.
+ *
+ * For simple fields whose value should be overwritten by the return value of a node,
+ * you don't need to define a reducer or default.
+ */
+ // additionalField: Annotation,
+});
diff --git a/langgraph/source/libs/cli/js-examples/static/studio.png b/langgraph/source/libs/cli/js-examples/static/studio.png
new file mode 100644
index 0000000000000000000000000000000000000000..8be3f14c8e3d06b3cc4de50706b848401ef52cec
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/static/studio.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8ed2a46973d6b7e7eda40ede2bc27beed41c2efcb746723e2ae736d5d36bf1ac
+size 596538
diff --git a/langgraph/source/libs/cli/js-examples/tests/agent.test.ts b/langgraph/source/libs/cli/js-examples/tests/agent.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d2948bbaebe9ee67d37aabc9aa13ab6d8872b182
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/tests/agent.test.ts
@@ -0,0 +1,8 @@
+import { describe, it, expect } from "@jest/globals";
+import { route } from "../src/agent/graph.js";
+describe("Routers", () => {
+ it("Test route", async () => {
+ const res = route({ messages: [] });
+ expect(res).toEqual("callModel");
+ }, 100_000);
+});
diff --git a/langgraph/source/libs/cli/js-examples/tests/graph.int.test.ts b/langgraph/source/libs/cli/js-examples/tests/graph.int.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a05978f4c9e4594616fdf29e29654dc5e6eefdd4
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/tests/graph.int.test.ts
@@ -0,0 +1,18 @@
+import { describe, it, expect } from "@jest/globals";
+import { graph } from "../src/agent/graph.js";
+
+describe("Graph", () => {
+ it("should process input through the graph", async () => {
+ const input = "What is the capital of France?";
+ const result = await graph.invoke({ input });
+
+ expect(result).toBeDefined();
+ expect(typeof result).toBe("object");
+ expect(result.messages).toBeDefined();
+ expect(Array.isArray(result.messages)).toBe(true);
+ expect(result.messages.length).toBeGreaterThan(0);
+
+ const lastMessage = result.messages[result.messages.length - 1];
+ expect(lastMessage.content.toString().toLowerCase()).toContain("hi");
+ }, 30000); // Increased timeout to 30 seconds
+});
diff --git a/langgraph/source/libs/cli/js-examples/tsconfig.json b/langgraph/source/libs/cli/js-examples/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..6e51abd3dd9ac8fc779b141fe45cc09823a198a7
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "extends": "@tsconfig/recommended",
+ "compilerOptions": {
+ "target": "ES2021",
+ "lib": ["ES2021", "ES2022.Object", "DOM"],
+ "module": "NodeNext",
+ "moduleResolution": "nodenext",
+ "esModuleInterop": true,
+ "noImplicitReturns": true,
+ "declaration": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "useDefineForClassFields": true,
+ "strictPropertyInitialization": false,
+ "allowJs": true,
+ "strict": true,
+ "strictFunctionTypes": false,
+ "outDir": "dist",
+ "types": ["jest", "node"],
+ "resolveJsonModule": true
+ },
+ "include": ["**/*.ts", "**/*.js"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/langgraph/source/libs/cli/js-examples/yarn.lock b/langgraph/source/libs/cli/js-examples/yarn.lock
new file mode 100644
index 0000000000000000000000000000000000000000..c3bb60b3698d6e3ece500d7e69fa9f65e30f154b
--- /dev/null
+++ b/langgraph/source/libs/cli/js-examples/yarn.lock
@@ -0,0 +1,3816 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@ampproject/remapping@^2.2.0":
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
+ integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.5"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465"
+ integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==
+ dependencies:
+ "@babel/highlight" "^7.24.7"
+ picocolors "^1.0.0"
+
+"@babel/compat-data@^7.25.2":
+ version "7.25.2"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.2.tgz#e41928bd33475305c586f6acbbb7e3ade7a6f7f5"
+ integrity sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==
+
+"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9":
+ version "7.25.2"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.2.tgz#ed8eec275118d7613e77a352894cd12ded8eba77"
+ integrity sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==
+ dependencies:
+ "@ampproject/remapping" "^2.2.0"
+ "@babel/code-frame" "^7.24.7"
+ "@babel/generator" "^7.25.0"
+ "@babel/helper-compilation-targets" "^7.25.2"
+ "@babel/helper-module-transforms" "^7.25.2"
+ "@babel/helpers" "^7.25.0"
+ "@babel/parser" "^7.25.0"
+ "@babel/template" "^7.25.0"
+ "@babel/traverse" "^7.25.2"
+ "@babel/types" "^7.25.2"
+ convert-source-map "^2.0.0"
+ debug "^4.1.0"
+ gensync "^1.0.0-beta.2"
+ json5 "^2.2.3"
+ semver "^6.3.1"
+
+"@babel/generator@^7.25.0", "@babel/generator@^7.7.2":
+ version "7.25.0"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.0.tgz#f858ddfa984350bc3d3b7f125073c9af6988f18e"
+ integrity sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==
+ dependencies:
+ "@babel/types" "^7.25.0"
+ "@jridgewell/gen-mapping" "^0.3.5"
+ "@jridgewell/trace-mapping" "^0.3.25"
+ jsesc "^2.5.1"
+
+"@babel/helper-compilation-targets@^7.25.2":
+ version "7.25.2"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz#e1d9410a90974a3a5a66e84ff55ef62e3c02d06c"
+ integrity sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==
+ dependencies:
+ "@babel/compat-data" "^7.25.2"
+ "@babel/helper-validator-option" "^7.24.8"
+ browserslist "^4.23.1"
+ lru-cache "^5.1.1"
+ semver "^6.3.1"
+
+"@babel/helper-module-imports@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b"
+ integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==
+ dependencies:
+ "@babel/traverse" "^7.24.7"
+ "@babel/types" "^7.24.7"
+
+"@babel/helper-module-transforms@^7.25.2":
+ version "7.25.2"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz#ee713c29768100f2776edf04d4eb23b8d27a66e6"
+ integrity sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==
+ dependencies:
+ "@babel/helper-module-imports" "^7.24.7"
+ "@babel/helper-simple-access" "^7.24.7"
+ "@babel/helper-validator-identifier" "^7.24.7"
+ "@babel/traverse" "^7.25.2"
+
+"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.8.0":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878"
+ integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==
+
+"@babel/helper-simple-access@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3"
+ integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==
+ dependencies:
+ "@babel/traverse" "^7.24.7"
+ "@babel/types" "^7.24.7"
+
+"@babel/helper-string-parser@^7.24.8":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d"
+ integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==
+
+"@babel/helper-validator-identifier@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db"
+ integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==
+
+"@babel/helper-validator-option@^7.24.8":
+ version "7.24.8"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d"
+ integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==
+
+"@babel/helpers@^7.25.0":
+ version "7.25.0"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.0.tgz#e69beb7841cb93a6505531ede34f34e6a073650a"
+ integrity sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==
+ dependencies:
+ "@babel/template" "^7.25.0"
+ "@babel/types" "^7.25.0"
+
+"@babel/highlight@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d"
+ integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.24.7"
+ chalk "^2.4.2"
+ js-tokens "^4.0.0"
+ picocolors "^1.0.0"
+
+"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.25.0", "@babel/parser@^7.25.3":
+ version "7.25.3"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.3.tgz#91fb126768d944966263f0657ab222a642b82065"
+ integrity sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==
+ dependencies:
+ "@babel/types" "^7.25.2"
+
+"@babel/plugin-syntax-async-generators@^7.8.4":
+ version "7.8.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
+ integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-bigint@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea"
+ integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-class-properties@^7.12.13":
+ version "7.12.13"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10"
+ integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.12.13"
+
+"@babel/plugin-syntax-class-static-block@^7.14.5":
+ version "7.14.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406"
+ integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.14.5"
+
+"@babel/plugin-syntax-import-attributes@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz#b4f9ea95a79e6912480c4b626739f86a076624ca"
+ integrity sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.24.7"
+
+"@babel/plugin-syntax-import-meta@^7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51"
+ integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-json-strings@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
+ integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-jsx@^7.7.2":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz#39a1fa4a7e3d3d7f34e2acc6be585b718d30e02d"
+ integrity sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.24.7"
+
+"@babel/plugin-syntax-logical-assignment-operators@^7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699"
+ integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
+ integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-numeric-separator@^7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97"
+ integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-object-rest-spread@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
+ integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-catch-binding@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
+ integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-chaining@^7.8.3":
+ version "7.8.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
+ integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-private-property-in-object@^7.14.5":
+ version "7.14.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad"
+ integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.14.5"
+
+"@babel/plugin-syntax-top-level-await@^7.14.5":
+ version "7.14.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c"
+ integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.14.5"
+
+"@babel/plugin-syntax-typescript@^7.7.2":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz#58d458271b4d3b6bb27ee6ac9525acbb259bad1c"
+ integrity sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.24.7"
+
+"@babel/template@^7.25.0", "@babel/template@^7.3.3":
+ version "7.25.0"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.0.tgz#e733dc3134b4fede528c15bc95e89cb98c52592a"
+ integrity sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==
+ dependencies:
+ "@babel/code-frame" "^7.24.7"
+ "@babel/parser" "^7.25.0"
+ "@babel/types" "^7.25.0"
+
+"@babel/traverse@^7.24.7", "@babel/traverse@^7.25.2":
+ version "7.25.3"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.3.tgz#f1b901951c83eda2f3e29450ce92743783373490"
+ integrity sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==
+ dependencies:
+ "@babel/code-frame" "^7.24.7"
+ "@babel/generator" "^7.25.0"
+ "@babel/parser" "^7.25.3"
+ "@babel/template" "^7.25.0"
+ "@babel/types" "^7.25.2"
+ debug "^4.3.1"
+ globals "^11.1.0"
+
+"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.24.7", "@babel/types@^7.25.0", "@babel/types@^7.25.2", "@babel/types@^7.3.3":
+ version "7.25.2"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.2.tgz#55fb231f7dc958cd69ea141a4c2997e819646125"
+ integrity sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==
+ dependencies:
+ "@babel/helper-string-parser" "^7.24.8"
+ "@babel/helper-validator-identifier" "^7.24.7"
+ to-fast-properties "^2.0.0"
+
+"@bcoe/v8-coverage@^0.2.3":
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
+ integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
+
+"@eslint-community/eslint-utils@^4.2.0":
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
+ integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
+ dependencies:
+ eslint-visitor-keys "^3.3.0"
+
+"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1":
+ version "4.11.0"
+ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae"
+ integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==
+
+"@eslint/eslintrc@^2.1.4":
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad"
+ integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==
+ dependencies:
+ ajv "^6.12.4"
+ debug "^4.3.2"
+ espree "^9.6.0"
+ globals "^13.19.0"
+ ignore "^5.2.0"
+ import-fresh "^3.2.1"
+ js-yaml "^4.1.0"
+ minimatch "^3.1.2"
+ strip-json-comments "^3.1.1"
+
+"@eslint/eslintrc@^3.1.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.1.0.tgz#dbd3482bfd91efa663cbe7aa1f506839868207b6"
+ integrity sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==
+ dependencies:
+ ajv "^6.12.4"
+ debug "^4.3.2"
+ espree "^10.0.1"
+ globals "^14.0.0"
+ ignore "^5.2.0"
+ import-fresh "^3.2.1"
+ js-yaml "^4.1.0"
+ minimatch "^3.1.2"
+ strip-json-comments "^3.1.1"
+
+"@eslint/js@8.57.0":
+ version "8.57.0"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f"
+ integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
+
+"@eslint/js@^9.9.1":
+ version "9.9.1"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.9.1.tgz#4a97e85e982099d6c7ee8410aacb55adaa576f06"
+ integrity sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==
+
+"@humanwhocodes/config-array@^0.11.14":
+ version "0.11.14"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"
+ integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==
+ dependencies:
+ "@humanwhocodes/object-schema" "^2.0.2"
+ debug "^4.3.1"
+ minimatch "^3.0.5"
+
+"@humanwhocodes/module-importer@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
+ integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
+
+"@humanwhocodes/object-schema@^2.0.2":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
+ integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
+
+"@istanbuljs/load-nyc-config@^1.0.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
+ integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==
+ dependencies:
+ camelcase "^5.3.1"
+ find-up "^4.1.0"
+ get-package-type "^0.1.0"
+ js-yaml "^3.13.1"
+ resolve-from "^5.0.0"
+
+"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3":
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
+ integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
+
+"@jest/console@^29.7.0":
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc"
+ integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==
+ dependencies:
+ "@jest/types" "^29.6.3"
+ "@types/node" "*"
+ chalk "^4.0.0"
+ jest-message-util "^29.7.0"
+ jest-util "^29.7.0"
+ slash "^3.0.0"
+
+"@jest/core@^29.7.0":
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f"
+ integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==
+ dependencies:
+ "@jest/console" "^29.7.0"
+ "@jest/reporters" "^29.7.0"
+ "@jest/test-result" "^29.7.0"
+ "@jest/transform" "^29.7.0"
+ "@jest/types" "^29.6.3"
+ "@types/node" "*"
+ ansi-escapes "^4.2.1"
+ chalk "^4.0.0"
+ ci-info "^3.2.0"
+ exit "^0.1.2"
+ graceful-fs "^4.2.9"
+ jest-changed-files "^29.7.0"
+ jest-config "^29.7.0"
+ jest-haste-map "^29.7.0"
+ jest-message-util "^29.7.0"
+ jest-regex-util "^29.6.3"
+ jest-resolve "^29.7.0"
+ jest-resolve-dependencies "^29.7.0"
+ jest-runner "^29.7.0"
+ jest-runtime "^29.7.0"
+ jest-snapshot "^29.7.0"
+ jest-util "^29.7.0"
+ jest-validate "^29.7.0"
+ jest-watcher "^29.7.0"
+ micromatch "^4.0.4"
+ pretty-format "^29.7.0"
+ slash "^3.0.0"
+ strip-ansi "^6.0.0"
+
+"@jest/environment@^29.7.0":
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7"
+ integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==
+ dependencies:
+ "@jest/fake-timers" "^29.7.0"
+ "@jest/types" "^29.6.3"
+ "@types/node" "*"
+ jest-mock "^29.7.0"
+
+"@jest/expect-utils@^29.7.0":
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6"
+ integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==
+ dependencies:
+ jest-get-type "^29.6.3"
+
+"@jest/expect@^29.7.0":
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2"
+ integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==
+ dependencies:
+ expect "^29.7.0"
+ jest-snapshot "^29.7.0"
+
+"@jest/fake-timers@^29.7.0":
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565"
+ integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==
+ dependencies:
+ "@jest/types" "^29.6.3"
+ "@sinonjs/fake-timers" "^10.0.2"
+ "@types/node" "*"
+ jest-message-util "^29.7.0"
+ jest-mock "^29.7.0"
+ jest-util "^29.7.0"
+
+"@jest/globals@^29.7.0":
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d"
+ integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==
+ dependencies:
+ "@jest/environment" "^29.7.0"
+ "@jest/expect" "^29.7.0"
+ "@jest/types" "^29.6.3"
+ jest-mock "^29.7.0"
+
+"@jest/reporters@^29.7.0":
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7"
+ integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==
+ dependencies:
+ "@bcoe/v8-coverage" "^0.2.3"
+ "@jest/console" "^29.7.0"
+ "@jest/test-result" "^29.7.0"
+ "@jest/transform" "^29.7.0"
+ "@jest/types" "^29.6.3"
+ "@jridgewell/trace-mapping" "^0.3.18"
+ "@types/node" "*"
+ chalk "^4.0.0"
+ collect-v8-coverage "^1.0.0"
+ exit "^0.1.2"
+ glob "^7.1.3"
+ graceful-fs "^4.2.9"
+ istanbul-lib-coverage "^3.0.0"
+ istanbul-lib-instrument "^6.0.0"
+ istanbul-lib-report "^3.0.0"
+ istanbul-lib-source-maps "^4.0.0"
+ istanbul-reports "^3.1.3"
+ jest-message-util "^29.7.0"
+ jest-util "^29.7.0"
+ jest-worker "^29.7.0"
+ slash "^3.0.0"
+ string-length "^4.0.1"
+ strip-ansi "^6.0.0"
+ v8-to-istanbul "^9.0.1"
+
+"@jest/schemas@^29.6.3":
+ version "29.6.3"
+ resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03"
+ integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==
+ dependencies:
+ "@sinclair/typebox" "^0.27.8"
+
+"@jest/source-map@^29.6.3":
+ version "29.6.3"
+ resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4"
+ integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==
+ dependencies:
+ "@jridgewell/trace-mapping" "^0.3.18"
+ callsites "^3.0.0"
+ graceful-fs "^4.2.9"
+
+"@jest/test-result@^29.7.0":
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c"
+ integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==
+ dependencies:
+ "@jest/console" "^29.7.0"
+ "@jest/types" "^29.6.3"
+ "@types/istanbul-lib-coverage" "^2.0.0"
+ collect-v8-coverage "^1.0.0"
+
+"@jest/test-sequencer@^29.7.0":
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce"
+ integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==
+ dependencies:
+ "@jest/test-result" "^29.7.0"
+ graceful-fs "^4.2.9"
+ jest-haste-map "^29.7.0"
+ slash "^3.0.0"
+
+"@jest/transform@^29.7.0":
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c"
+ integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==
+ dependencies:
+ "@babel/core" "^7.11.6"
+ "@jest/types" "^29.6.3"
+ "@jridgewell/trace-mapping" "^0.3.18"
+ babel-plugin-istanbul "^6.1.1"
+ chalk "^4.0.0"
+ convert-source-map "^2.0.0"
+ fast-json-stable-stringify "^2.1.0"
+ graceful-fs "^4.2.9"
+ jest-haste-map "^29.7.0"
+ jest-regex-util "^29.6.3"
+ jest-util "^29.7.0"
+ micromatch "^4.0.4"
+ pirates "^4.0.4"
+ slash "^3.0.0"
+ write-file-atomic "^4.0.2"
+
+"@jest/types@^29.6.3":
+ version "29.6.3"
+ resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59"
+ integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==
+ dependencies:
+ "@jest/schemas" "^29.6.3"
+ "@types/istanbul-lib-coverage" "^2.0.0"
+ "@types/istanbul-reports" "^3.0.0"
+ "@types/node" "*"
+ "@types/yargs" "^17.0.8"
+ chalk "^4.0.0"
+
+"@jridgewell/gen-mapping@^0.3.5":
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36"
+ integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==
+ dependencies:
+ "@jridgewell/set-array" "^1.2.1"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
+"@jridgewell/resolve-uri@^3.1.0":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
+ integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
+
+"@jridgewell/set-array@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280"
+ integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
+
+"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
+ integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
+
+"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
+ version "0.3.25"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
+ integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
+"@langchain/core@^0.3.2":
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.3.2.tgz#aff6d83149a40e0e735910f583aca0f1dd7d1bab"
+ integrity sha512-FeoDOStP8l1YdxgykpXnVoEnl4lxGNSOdYzUJN/EdFtkc6cIjDDS5+xewajme0+egaUsO4tGLezKaFpoWxAyQA==
+ dependencies:
+ ansi-styles "^5.0.0"
+ camelcase "6"
+ decamelize "1.2.0"
+ js-tiktoken "^1.0.12"
+ langsmith "^0.1.56"
+ mustache "^4.2.0"
+ p-queue "^6.6.2"
+ p-retry "4"
+ uuid "^10.0.0"
+ zod "^3.22.4"
+ zod-to-json-schema "^3.22.3"
+
+"@langchain/langgraph-checkpoint@~0.0.17":
+ version "0.0.17"
+ resolved "https://registry.yarnpkg.com/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.0.17.tgz#d0a8824eb0769567da54262adebe65db4ee6d58f"
+ integrity sha512-6b3CuVVYx+7x0uWLG+7YXz9j2iBa+tn2AXvkLxzEvaAsLE6Sij++8PPbS2BZzC+S/FPJdWsz6I5bsrqL0BYrCA==
+ dependencies:
+ uuid "^10.0.0"
+
+"@langchain/langgraph-sdk@~0.0.32":
+ version "0.0.70"
+ resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.70.tgz#9589f984b47de5e4a669b6008cbf01427a3d41d0"
+ integrity sha512-O8I12bfeMVz5fOrXnIcK4IdRf50IqyJTO458V56wAIHLNoi4H8/JHM+2M+Y4H2PtslXIGnvomWqlBd0eY5z/Og==
+ dependencies:
+ "@types/json-schema" "^7.0.15"
+ p-queue "^6.6.2"
+ p-retry "4"
+ uuid "^9.0.0"
+
+"@langchain/langgraph@^0.2.5":
+ version "0.2.67"
+ resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-0.2.67.tgz#f154e4e8534ca78b2b73a2cd3a901d82790b0d1b"
+ integrity sha512-tu/ewNIvhIPzeW5GxGzqjmGHinnU/qbNAoLM9czdpci0PCbMysbEJ2pbJrZs7ZjaReWSnr/THkeLPQwqGOM9xw==
+ dependencies:
+ "@langchain/langgraph-checkpoint" "~0.0.17"
+ "@langchain/langgraph-sdk" "~0.0.32"
+ uuid "^10.0.0"
+ zod "^3.23.8"
+
+"@nodelib/fs.scandir@2.1.5":
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+ integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+ dependencies:
+ "@nodelib/fs.stat" "2.0.5"
+ run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+ integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+ dependencies:
+ "@nodelib/fs.scandir" "2.1.5"
+ fastq "^1.6.0"
+
+"@sinclair/typebox@^0.27.8":
+ version "0.27.8"
+ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e"
+ integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==
+
+"@sinonjs/commons@^3.0.0":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd"
+ integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==
+ dependencies:
+ type-detect "4.0.8"
+
+"@sinonjs/fake-timers@^10.0.2":
+ version "10.3.0"
+ resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66"
+ integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==
+ dependencies:
+ "@sinonjs/commons" "^3.0.0"
+
+"@tsconfig/recommended@^1.0.7":
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/@tsconfig/recommended/-/recommended-1.0.7.tgz#fdd95fc2c8d643c8b4a8ca45fd68eea248512407"
+ integrity sha512-xiNMgCuoy4mCL4JTywk9XFs5xpRUcKxtWEcMR6FNMtsgewYTIgIR+nvlP4A4iRCAzRsHMnPhvTRrzp4AGcRTEA==
+
+"@types/babel__core@^7.1.14":
+ version "7.20.5"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"
+ integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==
+ dependencies:
+ "@babel/parser" "^7.20.7"
+ "@babel/types" "^7.20.7"
+ "@types/babel__generator" "*"
+ "@types/babel__template" "*"
+ "@types/babel__traverse" "*"
+
+"@types/babel__generator@*":
+ version "7.6.8"
+ resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab"
+ integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@types/babel__template@*":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f"
+ integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6":
+ version "7.20.6"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7"
+ integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==
+ dependencies:
+ "@babel/types" "^7.20.7"
+
+"@types/graceful-fs@^4.1.3":
+ version "4.1.9"
+ resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4"
+ integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==
+ dependencies:
+ "@types/node" "*"
+
+"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
+ integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==
+
+"@types/istanbul-lib-report@*":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf"
+ integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==
+ dependencies:
+ "@types/istanbul-lib-coverage" "*"
+
+"@types/istanbul-reports@^3.0.0":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54"
+ integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==
+ dependencies:
+ "@types/istanbul-lib-report" "*"
+
+"@types/jest@^29.5.0":
+ version "29.5.12"
+ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.12.tgz#7f7dc6eb4cf246d2474ed78744b05d06ce025544"
+ integrity sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==
+ dependencies:
+ expect "^29.0.0"
+ pretty-format "^29.0.0"
+
+"@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9":
+ version "7.0.15"
+ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
+ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
+
+"@types/json5@^0.0.29":
+ version "0.0.29"
+ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
+ integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
+
+"@types/node@*":
+ version "22.4.1"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-22.4.1.tgz#9b595d292c65b94c20923159e2ce947731b6fdce"
+ integrity sha512-1tbpb9325+gPnKK0dMm+/LMriX0vKxf6RnB0SZUqfyVkQ4fMgUSySqhxE/y8Jvs4NyF1yHzTfG9KlnkIODxPKg==
+ dependencies:
+ undici-types "~6.19.2"
+
+"@types/retry@0.12.0":
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
+ integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
+
+"@types/semver@^7.3.12":
+ version "7.5.8"
+ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
+ integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==
+
+"@types/stack-utils@^2.0.0":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"
+ integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==
+
+"@types/uuid@^10.0.0":
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d"
+ integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==
+
+"@types/yargs-parser@*":
+ version "21.0.3"
+ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15"
+ integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==
+
+"@types/yargs@^17.0.8":
+ version "17.0.33"
+ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d"
+ integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==
+ dependencies:
+ "@types/yargs-parser" "*"
+
+"@typescript-eslint/eslint-plugin@^5.59.8":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db"
+ integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==
+ dependencies:
+ "@eslint-community/regexpp" "^4.4.0"
+ "@typescript-eslint/scope-manager" "5.62.0"
+ "@typescript-eslint/type-utils" "5.62.0"
+ "@typescript-eslint/utils" "5.62.0"
+ debug "^4.3.4"
+ graphemer "^1.4.0"
+ ignore "^5.2.0"
+ natural-compare-lite "^1.4.0"
+ semver "^7.3.7"
+ tsutils "^3.21.0"
+
+"@typescript-eslint/parser@^5.59.8":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7"
+ integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==
+ dependencies:
+ "@typescript-eslint/scope-manager" "5.62.0"
+ "@typescript-eslint/types" "5.62.0"
+ "@typescript-eslint/typescript-estree" "5.62.0"
+ debug "^4.3.4"
+
+"@typescript-eslint/scope-manager@5.62.0":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c"
+ integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==
+ dependencies:
+ "@typescript-eslint/types" "5.62.0"
+ "@typescript-eslint/visitor-keys" "5.62.0"
+
+"@typescript-eslint/type-utils@5.62.0":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a"
+ integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==
+ dependencies:
+ "@typescript-eslint/typescript-estree" "5.62.0"
+ "@typescript-eslint/utils" "5.62.0"
+ debug "^4.3.4"
+ tsutils "^3.21.0"
+
+"@typescript-eslint/types@5.62.0":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f"
+ integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==
+
+"@typescript-eslint/typescript-estree@5.62.0":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b"
+ integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==
+ dependencies:
+ "@typescript-eslint/types" "5.62.0"
+ "@typescript-eslint/visitor-keys" "5.62.0"
+ debug "^4.3.4"
+ globby "^11.1.0"
+ is-glob "^4.0.3"
+ semver "^7.3.7"
+ tsutils "^3.21.0"
+
+"@typescript-eslint/utils@5.62.0":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86"
+ integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==
+ dependencies:
+ "@eslint-community/eslint-utils" "^4.2.0"
+ "@types/json-schema" "^7.0.9"
+ "@types/semver" "^7.3.12"
+ "@typescript-eslint/scope-manager" "5.62.0"
+ "@typescript-eslint/types" "5.62.0"
+ "@typescript-eslint/typescript-estree" "5.62.0"
+ eslint-scope "^5.1.1"
+ semver "^7.3.7"
+
+"@typescript-eslint/visitor-keys@5.62.0":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e"
+ integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==
+ dependencies:
+ "@typescript-eslint/types" "5.62.0"
+ eslint-visitor-keys "^3.3.0"
+
+"@ungap/structured-clone@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
+ integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
+
+acorn-jsx@^5.3.2:
+ version "5.3.2"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
+ integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
+
+acorn@^8.12.0, acorn@^8.9.0:
+ version "8.12.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
+ integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
+
+ajv@^6.12.4:
+ version "6.12.6"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+ integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+ansi-escapes@^4.2.1:
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
+ integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
+ dependencies:
+ type-fest "^0.21.3"
+
+ansi-regex@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+ integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ dependencies:
+ color-convert "^1.9.0"
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+ integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+ dependencies:
+ color-convert "^2.0.1"
+
+ansi-styles@^5.0.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
+ integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
+
+anymatch@^3.0.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
+ integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
+argparse@^1.0.7:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+ integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+ dependencies:
+ sprintf-js "~1.0.2"
+
+argparse@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+ integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+array-buffer-byte-length@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f"
+ integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==
+ dependencies:
+ call-bind "^1.0.5"
+ is-array-buffer "^3.0.4"
+
+array-includes@^3.1.7:
+ version "3.1.8"
+ resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d"
+ integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==
+ dependencies:
+ call-bind "^1.0.7"
+ define-properties "^1.2.1"
+ es-abstract "^1.23.2"
+ es-object-atoms "^1.0.0"
+ get-intrinsic "^1.2.4"
+ is-string "^1.0.7"
+
+array-union@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
+ integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+
+array.prototype.findlastindex@^1.2.3:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d"
+ integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==
+ dependencies:
+ call-bind "^1.0.7"
+ define-properties "^1.2.1"
+ es-abstract "^1.23.2"
+ es-errors "^1.3.0"
+ es-object-atoms "^1.0.0"
+ es-shim-unscopables "^1.0.2"
+
+array.prototype.flat@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18"
+ integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.2.0"
+ es-abstract "^1.22.1"
+ es-shim-unscopables "^1.0.0"
+
+array.prototype.flatmap@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527"
+ integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.2.0"
+ es-abstract "^1.22.1"
+ es-shim-unscopables "^1.0.0"
+
+arraybuffer.prototype.slice@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6"
+ integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==
+ dependencies:
+ array-buffer-byte-length "^1.0.1"
+ call-bind "^1.0.5"
+ define-properties "^1.2.1"
+ es-abstract "^1.22.3"
+ es-errors "^1.2.1"
+ get-intrinsic "^1.2.3"
+ is-array-buffer "^3.0.4"
+ is-shared-array-buffer "^1.0.2"
+
+async@^3.2.3:
+ version "3.2.6"
+ resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
+ integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
+
+available-typed-arrays@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
+ integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==
+ dependencies:
+ possible-typed-array-names "^1.0.0"
+
+babel-jest@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5"
+ integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==
+ dependencies:
+ "@jest/transform" "^29.7.0"
+ "@types/babel__core" "^7.1.14"
+ babel-plugin-istanbul "^6.1.1"
+ babel-preset-jest "^29.6.3"
+ chalk "^4.0.0"
+ graceful-fs "^4.2.9"
+ slash "^3.0.0"
+
+babel-plugin-istanbul@^6.1.1:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73"
+ integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@istanbuljs/load-nyc-config" "^1.0.0"
+ "@istanbuljs/schema" "^0.1.2"
+ istanbul-lib-instrument "^5.0.4"
+ test-exclude "^6.0.0"
+
+babel-plugin-jest-hoist@^29.6.3:
+ version "29.6.3"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626"
+ integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==
+ dependencies:
+ "@babel/template" "^7.3.3"
+ "@babel/types" "^7.3.3"
+ "@types/babel__core" "^7.1.14"
+ "@types/babel__traverse" "^7.0.6"
+
+babel-preset-current-node-syntax@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz#9a929eafece419612ef4ae4f60b1862ebad8ef30"
+ integrity sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==
+ dependencies:
+ "@babel/plugin-syntax-async-generators" "^7.8.4"
+ "@babel/plugin-syntax-bigint" "^7.8.3"
+ "@babel/plugin-syntax-class-properties" "^7.12.13"
+ "@babel/plugin-syntax-class-static-block" "^7.14.5"
+ "@babel/plugin-syntax-import-attributes" "^7.24.7"
+ "@babel/plugin-syntax-import-meta" "^7.10.4"
+ "@babel/plugin-syntax-json-strings" "^7.8.3"
+ "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
+ "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
+ "@babel/plugin-syntax-numeric-separator" "^7.10.4"
+ "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
+ "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
+ "@babel/plugin-syntax-optional-chaining" "^7.8.3"
+ "@babel/plugin-syntax-private-property-in-object" "^7.14.5"
+ "@babel/plugin-syntax-top-level-await" "^7.14.5"
+
+babel-preset-jest@^29.6.3:
+ version "29.6.3"
+ resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c"
+ integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==
+ dependencies:
+ babel-plugin-jest-hoist "^29.6.3"
+ babel-preset-current-node-syntax "^1.0.0"
+
+balanced-match@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+base64-js@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+ integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+brace-expansion@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
+ integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
+ dependencies:
+ balanced-match "^1.0.0"
+
+braces@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+ integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
+ dependencies:
+ fill-range "^7.1.1"
+
+browserslist@^4.23.1:
+ version "4.23.3"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800"
+ integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==
+ dependencies:
+ caniuse-lite "^1.0.30001646"
+ electron-to-chromium "^1.5.4"
+ node-releases "^2.0.18"
+ update-browserslist-db "^1.1.0"
+
+bs-logger@0.x:
+ version "0.2.6"
+ resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
+ integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==
+ dependencies:
+ fast-json-stable-stringify "2.x"
+
+bser@2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05"
+ integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==
+ dependencies:
+ node-int64 "^0.4.0"
+
+buffer-from@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+ integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
+ integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==
+ dependencies:
+ es-define-property "^1.0.0"
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+ get-intrinsic "^1.2.4"
+ set-function-length "^1.2.1"
+
+callsites@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+ integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+camelcase@6, camelcase@^6.2.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
+ integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
+
+camelcase@^5.3.1:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+ integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+caniuse-lite@^1.0.30001646:
+ version "1.0.30001651"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz#52de59529e8b02b1aedcaaf5c05d9e23c0c28138"
+ integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==
+
+chalk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+chalk@^4.0.0, chalk@^4.0.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+ integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+ dependencies:
+ ansi-styles "^4.1.0"
+ supports-color "^7.1.0"
+
+char-regex@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
+ integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
+
+ci-info@^3.2.0:
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4"
+ integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==
+
+cjs-module-lexer@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz#c485341ae8fd999ca4ee5af2d7a1c9ae01e0099c"
+ integrity sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==
+
+cliui@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
+ integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
+ dependencies:
+ string-width "^4.2.0"
+ strip-ansi "^6.0.1"
+ wrap-ansi "^7.0.0"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+ integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==
+
+collect-v8-coverage@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9"
+ integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==
+
+color-convert@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+ integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+ dependencies:
+ color-name "1.1.3"
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+ integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
+
+color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+commander@^10.0.1:
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
+ integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+convert-source-map@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
+ integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
+
+create-jest@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320"
+ integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==
+ dependencies:
+ "@jest/types" "^29.6.3"
+ chalk "^4.0.0"
+ exit "^0.1.2"
+ graceful-fs "^4.2.9"
+ jest-config "^29.7.0"
+ jest-util "^29.7.0"
+ prompts "^2.0.1"
+
+cross-spawn@^7.0.2, cross-spawn@^7.0.3:
+ 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"
+ which "^2.0.1"
+
+data-view-buffer@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2"
+ integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==
+ dependencies:
+ call-bind "^1.0.6"
+ es-errors "^1.3.0"
+ is-data-view "^1.0.1"
+
+data-view-byte-length@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2"
+ integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==
+ dependencies:
+ call-bind "^1.0.7"
+ es-errors "^1.3.0"
+ is-data-view "^1.0.1"
+
+data-view-byte-offset@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a"
+ integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==
+ dependencies:
+ call-bind "^1.0.6"
+ es-errors "^1.3.0"
+ is-data-view "^1.0.1"
+
+debug@^3.2.7:
+ version "3.2.7"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+ integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
+ dependencies:
+ ms "^2.1.1"
+
+debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
+ version "4.3.6"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b"
+ integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==
+ dependencies:
+ ms "2.1.2"
+
+decamelize@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+ integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
+
+dedent@^1.0.0:
+ version "1.5.3"
+ resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a"
+ integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==
+
+deep-is@^0.1.3:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
+ integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
+
+deepmerge@^4.2.2:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
+ integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
+
+define-data-property@^1.0.1, define-data-property@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
+ integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
+ dependencies:
+ es-define-property "^1.0.0"
+ es-errors "^1.3.0"
+ gopd "^1.0.1"
+
+define-properties@^1.2.0, define-properties@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c"
+ integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==
+ dependencies:
+ define-data-property "^1.0.1"
+ has-property-descriptors "^1.0.0"
+ object-keys "^1.1.1"
+
+detect-newline@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
+ integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
+
+diff-sequences@^29.6.3:
+ version "29.6.3"
+ resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921"
+ integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==
+
+dir-glob@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
+ integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
+ dependencies:
+ path-type "^4.0.0"
+
+doctrine@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
+ integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==
+ dependencies:
+ esutils "^2.0.2"
+
+doctrine@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
+ integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
+ dependencies:
+ esutils "^2.0.2"
+
+dotenv@^16.4.5:
+ version "16.4.5"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
+ integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
+
+ejs@^3.1.10:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b"
+ integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==
+ dependencies:
+ jake "^10.8.5"
+
+electron-to-chromium@^1.5.4:
+ version "1.5.12"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.12.tgz#ee31756eaa2e06f2aa606f170b7ad06dd402b4e4"
+ integrity sha512-tIhPkdlEoCL1Y+PToq3zRNehUaKp3wBX/sr7aclAWdIWjvqAe/Im/H0SiCM4c1Q8BLPHCdoJTol+ZblflydehA==
+
+emittery@^0.13.1:
+ version "0.13.1"
+ resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad"
+ integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==
+
+emoji-regex@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+ integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+error-ex@^1.3.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+ integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2:
+ version "1.23.3"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0"
+ integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==
+ dependencies:
+ array-buffer-byte-length "^1.0.1"
+ arraybuffer.prototype.slice "^1.0.3"
+ available-typed-arrays "^1.0.7"
+ call-bind "^1.0.7"
+ data-view-buffer "^1.0.1"
+ data-view-byte-length "^1.0.1"
+ data-view-byte-offset "^1.0.0"
+ es-define-property "^1.0.0"
+ es-errors "^1.3.0"
+ es-object-atoms "^1.0.0"
+ es-set-tostringtag "^2.0.3"
+ es-to-primitive "^1.2.1"
+ function.prototype.name "^1.1.6"
+ get-intrinsic "^1.2.4"
+ get-symbol-description "^1.0.2"
+ globalthis "^1.0.3"
+ gopd "^1.0.1"
+ has-property-descriptors "^1.0.2"
+ has-proto "^1.0.3"
+ has-symbols "^1.0.3"
+ hasown "^2.0.2"
+ internal-slot "^1.0.7"
+ is-array-buffer "^3.0.4"
+ is-callable "^1.2.7"
+ is-data-view "^1.0.1"
+ is-negative-zero "^2.0.3"
+ is-regex "^1.1.4"
+ is-shared-array-buffer "^1.0.3"
+ is-string "^1.0.7"
+ is-typed-array "^1.1.13"
+ is-weakref "^1.0.2"
+ object-inspect "^1.13.1"
+ object-keys "^1.1.1"
+ object.assign "^4.1.5"
+ regexp.prototype.flags "^1.5.2"
+ safe-array-concat "^1.1.2"
+ safe-regex-test "^1.0.3"
+ string.prototype.trim "^1.2.9"
+ string.prototype.trimend "^1.0.8"
+ string.prototype.trimstart "^1.0.8"
+ typed-array-buffer "^1.0.2"
+ typed-array-byte-length "^1.0.1"
+ typed-array-byte-offset "^1.0.2"
+ typed-array-length "^1.0.6"
+ unbox-primitive "^1.0.2"
+ which-typed-array "^1.1.15"
+
+es-define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845"
+ integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==
+ dependencies:
+ get-intrinsic "^1.2.4"
+
+es-errors@^1.2.1, es-errors@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+ integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
+es-object-atoms@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941"
+ integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==
+ dependencies:
+ es-errors "^1.3.0"
+
+es-set-tostringtag@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777"
+ integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==
+ dependencies:
+ get-intrinsic "^1.2.4"
+ has-tostringtag "^1.0.2"
+ hasown "^2.0.1"
+
+es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763"
+ integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==
+ dependencies:
+ hasown "^2.0.0"
+
+es-to-primitive@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+ integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+ dependencies:
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.2"
+
+escalade@^3.1.1, escalade@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27"
+ integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==
+
+escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+ integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
+
+escape-string-regexp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
+ integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
+
+escape-string-regexp@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+ integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+eslint-config-prettier@^8.8.0:
+ version "8.10.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11"
+ integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==
+
+eslint-import-resolver-node@^0.3.9:
+ version "0.3.9"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac"
+ integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==
+ dependencies:
+ debug "^3.2.7"
+ is-core-module "^2.13.0"
+ resolve "^1.22.4"
+
+eslint-module-utils@^2.8.0:
+ version "2.8.2"
+ resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.2.tgz#2ecad69d71e1fa81f17f7f24d5d3e46b168de663"
+ integrity sha512-3XnC5fDyc8M4J2E8pt8pmSVRX2M+5yWMCfI/kDZwauQeFgzQOuhcRBFKjTeJagqgk4sFKxe1mvNVnaWwImx/Tg==
+ dependencies:
+ debug "^3.2.7"
+
+eslint-plugin-import@^2.27.5:
+ version "2.29.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643"
+ integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==
+ dependencies:
+ array-includes "^3.1.7"
+ array.prototype.findlastindex "^1.2.3"
+ array.prototype.flat "^1.3.2"
+ array.prototype.flatmap "^1.3.2"
+ debug "^3.2.7"
+ doctrine "^2.1.0"
+ eslint-import-resolver-node "^0.3.9"
+ eslint-module-utils "^2.8.0"
+ hasown "^2.0.0"
+ is-core-module "^2.13.1"
+ is-glob "^4.0.3"
+ minimatch "^3.1.2"
+ object.fromentries "^2.0.7"
+ object.groupby "^1.0.1"
+ object.values "^1.1.7"
+ semver "^6.3.1"
+ tsconfig-paths "^3.15.0"
+
+eslint-plugin-no-instanceof@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-no-instanceof/-/eslint-plugin-no-instanceof-1.0.1.tgz#5d9fc86d160df6991b654b294a62390207f1bb97"
+ integrity sha512-zlqQ7EsfzbRO68uI+p8FIE7zYB4njs+nNbkNjSb5QmLi2et67zQLqSeaao5U9SpnlZTTJC87nS2oyHo2ACtajw==
+
+eslint-plugin-prettier@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b"
+ integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==
+ dependencies:
+ prettier-linter-helpers "^1.0.0"
+
+eslint-scope@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+ integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
+ dependencies:
+ esrecurse "^4.3.0"
+ estraverse "^4.1.1"
+
+eslint-scope@^7.2.2:
+ version "7.2.2"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
+ integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
+ dependencies:
+ esrecurse "^4.3.0"
+ estraverse "^5.2.0"
+
+eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
+ version "3.4.3"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
+ integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
+
+eslint-visitor-keys@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb"
+ integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==
+
+eslint@^8.41.0:
+ version "8.57.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668"
+ integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==
+ dependencies:
+ "@eslint-community/eslint-utils" "^4.2.0"
+ "@eslint-community/regexpp" "^4.6.1"
+ "@eslint/eslintrc" "^2.1.4"
+ "@eslint/js" "8.57.0"
+ "@humanwhocodes/config-array" "^0.11.14"
+ "@humanwhocodes/module-importer" "^1.0.1"
+ "@nodelib/fs.walk" "^1.2.8"
+ "@ungap/structured-clone" "^1.2.0"
+ ajv "^6.12.4"
+ chalk "^4.0.0"
+ cross-spawn "^7.0.2"
+ debug "^4.3.2"
+ doctrine "^3.0.0"
+ escape-string-regexp "^4.0.0"
+ eslint-scope "^7.2.2"
+ eslint-visitor-keys "^3.4.3"
+ espree "^9.6.1"
+ esquery "^1.4.2"
+ esutils "^2.0.2"
+ fast-deep-equal "^3.1.3"
+ file-entry-cache "^6.0.1"
+ find-up "^5.0.0"
+ glob-parent "^6.0.2"
+ globals "^13.19.0"
+ graphemer "^1.4.0"
+ ignore "^5.2.0"
+ imurmurhash "^0.1.4"
+ is-glob "^4.0.0"
+ is-path-inside "^3.0.3"
+ js-yaml "^4.1.0"
+ json-stable-stringify-without-jsonify "^1.0.1"
+ levn "^0.4.1"
+ lodash.merge "^4.6.2"
+ minimatch "^3.1.2"
+ natural-compare "^1.4.0"
+ optionator "^0.9.3"
+ strip-ansi "^6.0.1"
+ text-table "^0.2.0"
+
+espree@^10.0.1:
+ version "10.1.0"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-10.1.0.tgz#8788dae611574c0f070691f522e4116c5a11fc56"
+ integrity sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==
+ dependencies:
+ acorn "^8.12.0"
+ acorn-jsx "^5.3.2"
+ eslint-visitor-keys "^4.0.0"
+
+espree@^9.6.0, espree@^9.6.1:
+ version "9.6.1"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
+ integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
+ dependencies:
+ acorn "^8.9.0"
+ acorn-jsx "^5.3.2"
+ eslint-visitor-keys "^3.4.1"
+
+esprima@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+ integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+esquery@^1.4.2:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7"
+ integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==
+ dependencies:
+ estraverse "^5.1.0"
+
+esrecurse@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+ integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+ dependencies:
+ estraverse "^5.2.0"
+
+estraverse@^4.1.1:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+ integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.1.0, estraverse@^5.2.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
+ integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+esutils@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+ integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+eventemitter3@^4.0.4:
+ version "4.0.7"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+ integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
+execa@^5.0.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
+ integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
+ dependencies:
+ cross-spawn "^7.0.3"
+ get-stream "^6.0.0"
+ human-signals "^2.1.0"
+ is-stream "^2.0.0"
+ merge-stream "^2.0.0"
+ npm-run-path "^4.0.1"
+ onetime "^5.1.2"
+ signal-exit "^3.0.3"
+ strip-final-newline "^2.0.0"
+
+exit@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
+ integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==
+
+expect@^29.0.0, expect@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc"
+ integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==
+ dependencies:
+ "@jest/expect-utils" "^29.7.0"
+ jest-get-type "^29.6.3"
+ jest-matcher-utils "^29.7.0"
+ jest-message-util "^29.7.0"
+ jest-util "^29.7.0"
+
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+ integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-diff@^1.1.2:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
+ integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
+
+fast-glob@^3.2.9:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
+ integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
+ dependencies:
+ "@nodelib/fs.stat" "^2.0.2"
+ "@nodelib/fs.walk" "^1.2.3"
+ glob-parent "^5.1.2"
+ merge2 "^1.3.0"
+ micromatch "^4.0.4"
+
+fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+ integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fast-levenshtein@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+ integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
+
+fastq@^1.6.0:
+ version "1.17.1"
+ resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"
+ integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==
+ dependencies:
+ reusify "^1.0.4"
+
+fb-watchman@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c"
+ integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==
+ dependencies:
+ bser "2.1.1"
+
+file-entry-cache@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
+ integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
+ dependencies:
+ flat-cache "^3.0.4"
+
+filelist@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5"
+ integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==
+ dependencies:
+ minimatch "^5.0.1"
+
+fill-range@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
+ integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+find-up@^4.0.0, find-up@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+ integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+ dependencies:
+ locate-path "^5.0.0"
+ path-exists "^4.0.0"
+
+find-up@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+ integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+ dependencies:
+ locate-path "^6.0.0"
+ path-exists "^4.0.0"
+
+flat-cache@^3.0.4:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee"
+ integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==
+ dependencies:
+ flatted "^3.2.9"
+ keyv "^4.5.3"
+ rimraf "^3.0.2"
+
+flatted@^3.2.9:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
+ integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==
+
+for-each@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
+ integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==
+ dependencies:
+ is-callable "^1.1.3"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+fsevents@^2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+
+function.prototype.name@^1.1.6:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd"
+ integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.2.0"
+ es-abstract "^1.22.1"
+ functions-have-names "^1.2.3"
+
+functions-have-names@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
+ integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
+
+gensync@^1.0.0-beta.2:
+ version "1.0.0-beta.2"
+ resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
+ integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
+
+get-caller-file@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+ integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
+ integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
+ dependencies:
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+ has-proto "^1.0.1"
+ has-symbols "^1.0.3"
+ hasown "^2.0.0"
+
+get-package-type@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
+ integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
+
+get-stream@^6.0.0:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
+ integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
+
+get-symbol-description@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5"
+ integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==
+ dependencies:
+ call-bind "^1.0.5"
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.4"
+
+glob-parent@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+glob-parent@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
+ integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
+ dependencies:
+ is-glob "^4.0.3"
+
+glob@^7.1.3, glob@^7.1.4:
+ version "7.2.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
+ integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.1.1"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+globals@^11.1.0:
+ version "11.12.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
+ integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
+
+globals@^13.19.0:
+ version "13.24.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171"
+ integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==
+ dependencies:
+ type-fest "^0.20.2"
+
+globals@^14.0.0:
+ version "14.0.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
+ integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
+
+globalthis@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236"
+ integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==
+ dependencies:
+ define-properties "^1.2.1"
+ gopd "^1.0.1"
+
+globby@^11.1.0:
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
+ integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
+ dependencies:
+ array-union "^2.1.0"
+ dir-glob "^3.0.1"
+ fast-glob "^3.2.9"
+ ignore "^5.2.0"
+ merge2 "^1.4.1"
+ slash "^3.0.0"
+
+gopd@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
+ integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
+ dependencies:
+ get-intrinsic "^1.1.3"
+
+graceful-fs@^4.2.9:
+ version "4.2.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
+graphemer@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
+ integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
+
+has-bigints@^1.0.1, has-bigints@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
+ integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+ integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
+
+has-flag@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+ integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
+ integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
+ dependencies:
+ es-define-property "^1.0.0"
+
+has-proto@^1.0.1, has-proto@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd"
+ integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==
+
+has-symbols@^1.0.2, has-symbols@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
+ integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
+
+has-tostringtag@^1.0.0, has-tostringtag@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
+ integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
+ dependencies:
+ has-symbols "^1.0.3"
+
+hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
+ integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
+ dependencies:
+ function-bind "^1.1.2"
+
+html-escaper@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
+ integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
+
+human-signals@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
+ integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
+
+ignore@^5.2.0:
+ version "5.3.2"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
+ integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
+
+import-fresh@^3.2.1:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
+ integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
+ dependencies:
+ parent-module "^1.0.0"
+ resolve-from "^4.0.0"
+
+import-local@^3.0.2:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260"
+ integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==
+ dependencies:
+ pkg-dir "^4.2.0"
+ resolve-cwd "^3.0.0"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+ integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+internal-slot@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802"
+ integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==
+ dependencies:
+ es-errors "^1.3.0"
+ hasown "^2.0.0"
+ side-channel "^1.0.4"
+
+is-array-buffer@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98"
+ integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==
+ dependencies:
+ call-bind "^1.0.2"
+ get-intrinsic "^1.2.1"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+ integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
+
+is-bigint@^1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
+ integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==
+ dependencies:
+ has-bigints "^1.0.1"
+
+is-boolean-object@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719"
+ integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==
+ dependencies:
+ call-bind "^1.0.2"
+ has-tostringtag "^1.0.0"
+
+is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
+ integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
+
+is-core-module@^2.13.0:
+ version "2.15.0"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea"
+ integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==
+ dependencies:
+ hasown "^2.0.2"
+
+is-core-module@^2.13.1:
+ version "2.15.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37"
+ integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==
+ dependencies:
+ hasown "^2.0.2"
+
+is-data-view@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f"
+ integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==
+ dependencies:
+ is-typed-array "^1.1.13"
+
+is-date-object@^1.0.1:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
+ integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
+ dependencies:
+ has-tostringtag "^1.0.0"
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-fullwidth-code-point@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+ integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-generator-fn@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
+ integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==
+
+is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-negative-zero@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747"
+ integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==
+
+is-number-object@^1.0.4:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc"
+ integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==
+ dependencies:
+ has-tostringtag "^1.0.0"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-path-inside@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
+ integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
+
+is-regex@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
+ integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
+ dependencies:
+ call-bind "^1.0.2"
+ has-tostringtag "^1.0.0"
+
+is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688"
+ integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==
+ dependencies:
+ call-bind "^1.0.7"
+
+is-stream@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
+ integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
+
+is-string@^1.0.5, is-string@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
+ integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
+ dependencies:
+ has-tostringtag "^1.0.0"
+
+is-symbol@^1.0.2, is-symbol@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
+ integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
+ dependencies:
+ has-symbols "^1.0.2"
+
+is-typed-array@^1.1.13:
+ version "1.1.13"
+ resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229"
+ integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==
+ dependencies:
+ which-typed-array "^1.1.14"
+
+is-weakref@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
+ integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==
+ dependencies:
+ call-bind "^1.0.2"
+
+isarray@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
+ integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756"
+ integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==
+
+istanbul-lib-instrument@^5.0.4:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d"
+ integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==
+ dependencies:
+ "@babel/core" "^7.12.3"
+ "@babel/parser" "^7.14.7"
+ "@istanbuljs/schema" "^0.1.2"
+ istanbul-lib-coverage "^3.2.0"
+ semver "^6.3.0"
+
+istanbul-lib-instrument@^6.0.0:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765"
+ integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==
+ dependencies:
+ "@babel/core" "^7.23.9"
+ "@babel/parser" "^7.23.9"
+ "@istanbuljs/schema" "^0.1.3"
+ istanbul-lib-coverage "^3.2.0"
+ semver "^7.5.4"
+
+istanbul-lib-report@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d"
+ integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==
+ dependencies:
+ istanbul-lib-coverage "^3.0.0"
+ make-dir "^4.0.0"
+ supports-color "^7.1.0"
+
+istanbul-lib-source-maps@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551"
+ integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==
+ dependencies:
+ debug "^4.1.1"
+ istanbul-lib-coverage "^3.0.0"
+ source-map "^0.6.1"
+
+istanbul-reports@^3.1.3:
+ version "3.1.7"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b"
+ integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==
+ dependencies:
+ html-escaper "^2.0.0"
+ istanbul-lib-report "^3.0.0"
+
+jake@^10.8.5:
+ version "10.9.2"
+ resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f"
+ integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==
+ dependencies:
+ async "^3.2.3"
+ chalk "^4.0.2"
+ filelist "^1.0.4"
+ minimatch "^3.1.2"
+
+jest-changed-files@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a"
+ integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==
+ dependencies:
+ execa "^5.0.0"
+ jest-util "^29.7.0"
+ p-limit "^3.1.0"
+
+jest-circus@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a"
+ integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==
+ dependencies:
+ "@jest/environment" "^29.7.0"
+ "@jest/expect" "^29.7.0"
+ "@jest/test-result" "^29.7.0"
+ "@jest/types" "^29.6.3"
+ "@types/node" "*"
+ chalk "^4.0.0"
+ co "^4.6.0"
+ dedent "^1.0.0"
+ is-generator-fn "^2.0.0"
+ jest-each "^29.7.0"
+ jest-matcher-utils "^29.7.0"
+ jest-message-util "^29.7.0"
+ jest-runtime "^29.7.0"
+ jest-snapshot "^29.7.0"
+ jest-util "^29.7.0"
+ p-limit "^3.1.0"
+ pretty-format "^29.7.0"
+ pure-rand "^6.0.0"
+ slash "^3.0.0"
+ stack-utils "^2.0.3"
+
+jest-cli@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995"
+ integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==
+ dependencies:
+ "@jest/core" "^29.7.0"
+ "@jest/test-result" "^29.7.0"
+ "@jest/types" "^29.6.3"
+ chalk "^4.0.0"
+ create-jest "^29.7.0"
+ exit "^0.1.2"
+ import-local "^3.0.2"
+ jest-config "^29.7.0"
+ jest-util "^29.7.0"
+ jest-validate "^29.7.0"
+ yargs "^17.3.1"
+
+jest-config@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f"
+ integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==
+ dependencies:
+ "@babel/core" "^7.11.6"
+ "@jest/test-sequencer" "^29.7.0"
+ "@jest/types" "^29.6.3"
+ babel-jest "^29.7.0"
+ chalk "^4.0.0"
+ ci-info "^3.2.0"
+ deepmerge "^4.2.2"
+ glob "^7.1.3"
+ graceful-fs "^4.2.9"
+ jest-circus "^29.7.0"
+ jest-environment-node "^29.7.0"
+ jest-get-type "^29.6.3"
+ jest-regex-util "^29.6.3"
+ jest-resolve "^29.7.0"
+ jest-runner "^29.7.0"
+ jest-util "^29.7.0"
+ jest-validate "^29.7.0"
+ micromatch "^4.0.4"
+ parse-json "^5.2.0"
+ pretty-format "^29.7.0"
+ slash "^3.0.0"
+ strip-json-comments "^3.1.1"
+
+jest-diff@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a"
+ integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==
+ dependencies:
+ chalk "^4.0.0"
+ diff-sequences "^29.6.3"
+ jest-get-type "^29.6.3"
+ pretty-format "^29.7.0"
+
+jest-docblock@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a"
+ integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==
+ dependencies:
+ detect-newline "^3.0.0"
+
+jest-each@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1"
+ integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==
+ dependencies:
+ "@jest/types" "^29.6.3"
+ chalk "^4.0.0"
+ jest-get-type "^29.6.3"
+ jest-util "^29.7.0"
+ pretty-format "^29.7.0"
+
+jest-environment-node@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376"
+ integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==
+ dependencies:
+ "@jest/environment" "^29.7.0"
+ "@jest/fake-timers" "^29.7.0"
+ "@jest/types" "^29.6.3"
+ "@types/node" "*"
+ jest-mock "^29.7.0"
+ jest-util "^29.7.0"
+
+jest-get-type@^29.6.3:
+ version "29.6.3"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1"
+ integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==
+
+jest-haste-map@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104"
+ integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==
+ dependencies:
+ "@jest/types" "^29.6.3"
+ "@types/graceful-fs" "^4.1.3"
+ "@types/node" "*"
+ anymatch "^3.0.3"
+ fb-watchman "^2.0.0"
+ graceful-fs "^4.2.9"
+ jest-regex-util "^29.6.3"
+ jest-util "^29.7.0"
+ jest-worker "^29.7.0"
+ micromatch "^4.0.4"
+ walker "^1.0.8"
+ optionalDependencies:
+ fsevents "^2.3.2"
+
+jest-leak-detector@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728"
+ integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==
+ dependencies:
+ jest-get-type "^29.6.3"
+ pretty-format "^29.7.0"
+
+jest-matcher-utils@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12"
+ integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==
+ dependencies:
+ chalk "^4.0.0"
+ jest-diff "^29.7.0"
+ jest-get-type "^29.6.3"
+ pretty-format "^29.7.0"
+
+jest-message-util@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3"
+ integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==
+ dependencies:
+ "@babel/code-frame" "^7.12.13"
+ "@jest/types" "^29.6.3"
+ "@types/stack-utils" "^2.0.0"
+ chalk "^4.0.0"
+ graceful-fs "^4.2.9"
+ micromatch "^4.0.4"
+ pretty-format "^29.7.0"
+ slash "^3.0.0"
+ stack-utils "^2.0.3"
+
+jest-mock@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347"
+ integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==
+ dependencies:
+ "@jest/types" "^29.6.3"
+ "@types/node" "*"
+ jest-util "^29.7.0"
+
+jest-pnp-resolver@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e"
+ integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==
+
+jest-regex-util@^29.6.3:
+ version "29.6.3"
+ resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52"
+ integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==
+
+jest-resolve-dependencies@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428"
+ integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==
+ dependencies:
+ jest-regex-util "^29.6.3"
+ jest-snapshot "^29.7.0"
+
+jest-resolve@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30"
+ integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==
+ dependencies:
+ chalk "^4.0.0"
+ graceful-fs "^4.2.9"
+ jest-haste-map "^29.7.0"
+ jest-pnp-resolver "^1.2.2"
+ jest-util "^29.7.0"
+ jest-validate "^29.7.0"
+ resolve "^1.20.0"
+ resolve.exports "^2.0.0"
+ slash "^3.0.0"
+
+jest-runner@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e"
+ integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==
+ dependencies:
+ "@jest/console" "^29.7.0"
+ "@jest/environment" "^29.7.0"
+ "@jest/test-result" "^29.7.0"
+ "@jest/transform" "^29.7.0"
+ "@jest/types" "^29.6.3"
+ "@types/node" "*"
+ chalk "^4.0.0"
+ emittery "^0.13.1"
+ graceful-fs "^4.2.9"
+ jest-docblock "^29.7.0"
+ jest-environment-node "^29.7.0"
+ jest-haste-map "^29.7.0"
+ jest-leak-detector "^29.7.0"
+ jest-message-util "^29.7.0"
+ jest-resolve "^29.7.0"
+ jest-runtime "^29.7.0"
+ jest-util "^29.7.0"
+ jest-watcher "^29.7.0"
+ jest-worker "^29.7.0"
+ p-limit "^3.1.0"
+ source-map-support "0.5.13"
+
+jest-runtime@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817"
+ integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==
+ dependencies:
+ "@jest/environment" "^29.7.0"
+ "@jest/fake-timers" "^29.7.0"
+ "@jest/globals" "^29.7.0"
+ "@jest/source-map" "^29.6.3"
+ "@jest/test-result" "^29.7.0"
+ "@jest/transform" "^29.7.0"
+ "@jest/types" "^29.6.3"
+ "@types/node" "*"
+ chalk "^4.0.0"
+ cjs-module-lexer "^1.0.0"
+ collect-v8-coverage "^1.0.0"
+ glob "^7.1.3"
+ graceful-fs "^4.2.9"
+ jest-haste-map "^29.7.0"
+ jest-message-util "^29.7.0"
+ jest-mock "^29.7.0"
+ jest-regex-util "^29.6.3"
+ jest-resolve "^29.7.0"
+ jest-snapshot "^29.7.0"
+ jest-util "^29.7.0"
+ slash "^3.0.0"
+ strip-bom "^4.0.0"
+
+jest-snapshot@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5"
+ integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==
+ dependencies:
+ "@babel/core" "^7.11.6"
+ "@babel/generator" "^7.7.2"
+ "@babel/plugin-syntax-jsx" "^7.7.2"
+ "@babel/plugin-syntax-typescript" "^7.7.2"
+ "@babel/types" "^7.3.3"
+ "@jest/expect-utils" "^29.7.0"
+ "@jest/transform" "^29.7.0"
+ "@jest/types" "^29.6.3"
+ babel-preset-current-node-syntax "^1.0.0"
+ chalk "^4.0.0"
+ expect "^29.7.0"
+ graceful-fs "^4.2.9"
+ jest-diff "^29.7.0"
+ jest-get-type "^29.6.3"
+ jest-matcher-utils "^29.7.0"
+ jest-message-util "^29.7.0"
+ jest-util "^29.7.0"
+ natural-compare "^1.4.0"
+ pretty-format "^29.7.0"
+ semver "^7.5.3"
+
+jest-util@^29.0.0, jest-util@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc"
+ integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==
+ dependencies:
+ "@jest/types" "^29.6.3"
+ "@types/node" "*"
+ chalk "^4.0.0"
+ ci-info "^3.2.0"
+ graceful-fs "^4.2.9"
+ picomatch "^2.2.3"
+
+jest-validate@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c"
+ integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==
+ dependencies:
+ "@jest/types" "^29.6.3"
+ camelcase "^6.2.0"
+ chalk "^4.0.0"
+ jest-get-type "^29.6.3"
+ leven "^3.1.0"
+ pretty-format "^29.7.0"
+
+jest-watcher@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2"
+ integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==
+ dependencies:
+ "@jest/test-result" "^29.7.0"
+ "@jest/types" "^29.6.3"
+ "@types/node" "*"
+ ansi-escapes "^4.2.1"
+ chalk "^4.0.0"
+ emittery "^0.13.1"
+ jest-util "^29.7.0"
+ string-length "^4.0.1"
+
+jest-worker@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a"
+ integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==
+ dependencies:
+ "@types/node" "*"
+ jest-util "^29.7.0"
+ merge-stream "^2.0.0"
+ supports-color "^8.0.0"
+
+jest@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613"
+ integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==
+ dependencies:
+ "@jest/core" "^29.7.0"
+ "@jest/types" "^29.6.3"
+ import-local "^3.0.2"
+ jest-cli "^29.7.0"
+
+js-tiktoken@^1.0.12:
+ version "1.0.14"
+ resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.14.tgz#756f353262d559da16b58b5bcecfd93330076da2"
+ integrity sha512-Pk3l3WOgM9joguZY2k52+jH82RtABRgB5RdGFZNUGbOKGMVlNmafcPA3b0ITcCZPu1L9UclP1tne6aw7ZI4Myg==
+ dependencies:
+ base64-js "^1.5.1"
+
+js-tokens@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@^3.13.1:
+ version "3.14.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
+ integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+js-yaml@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+ integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+ dependencies:
+ argparse "^2.0.1"
+
+jsesc@^2.5.1:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
+ integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+
+json-buffer@3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
+ integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
+
+json-parse-even-better-errors@^2.3.0:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+ integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-stable-stringify-without-jsonify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+ integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
+
+json5@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
+ integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
+ dependencies:
+ minimist "^1.2.0"
+
+json5@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
+ integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+
+keyv@^4.5.3:
+ version "4.5.4"
+ resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
+ integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
+ dependencies:
+ json-buffer "3.0.1"
+
+kleur@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
+ integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
+
+langsmith@^0.1.56:
+ version "0.1.58"
+ resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.1.58.tgz#502aa6c22fecd15fa65c14ffbe7fcc4643201f47"
+ integrity sha512-crbJbfw6hLBbVDQlMRWRVYwppApiDMncsqqBtTP1udUvilAsw4btIpBq0Tf+Jr8iQs6cEpZr/h7lGr5DdzAGew==
+ dependencies:
+ "@types/uuid" "^10.0.0"
+ commander "^10.0.1"
+ p-queue "^6.6.2"
+ p-retry "4"
+ semver "^7.6.3"
+ uuid "^10.0.0"
+
+leven@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
+ integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
+
+levn@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
+ integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
+ dependencies:
+ prelude-ls "^1.2.1"
+ type-check "~0.4.0"
+
+lines-and-columns@^1.1.6:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
+ integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
+
+locate-path@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+ integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+ dependencies:
+ p-locate "^4.1.0"
+
+locate-path@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
+ integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+ dependencies:
+ p-locate "^5.0.0"
+
+lodash.memoize@4.x:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+ integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==
+
+lodash.merge@^4.6.2:
+ version "4.6.2"
+ resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
+ integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
+
+lru-cache@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+ integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+ dependencies:
+ yallist "^3.0.2"
+
+make-dir@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e"
+ integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==
+ dependencies:
+ semver "^7.5.3"
+
+make-error@1.x:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
+ integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+
+makeerror@1.0.12:
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a"
+ integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==
+ dependencies:
+ tmpl "1.0.5"
+
+merge-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+ integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+merge2@^1.3.0, merge2@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+ integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.4:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
+ integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
+ dependencies:
+ braces "^3.0.3"
+ picomatch "^2.3.1"
+
+mimic-fn@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+ integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
+minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimatch@^5.0.1:
+ version "5.1.6"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
+ integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
+ dependencies:
+ brace-expansion "^2.0.1"
+
+minimist@^1.2.0, minimist@^1.2.6:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+ integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
+ms@2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+ integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+ms@^2.1.1:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+mustache@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
+ integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
+
+natural-compare-lite@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4"
+ integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+ integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
+
+node-int64@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+ integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==
+
+node-releases@^2.0.18:
+ version "2.0.18"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f"
+ integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==
+
+normalize-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+npm-run-path@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
+ integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
+ dependencies:
+ path-key "^3.0.0"
+
+object-inspect@^1.13.1:
+ version "1.13.2"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff"
+ integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==
+
+object-keys@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+ integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object.assign@^4.1.5:
+ version "4.1.5"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0"
+ integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==
+ dependencies:
+ call-bind "^1.0.5"
+ define-properties "^1.2.1"
+ has-symbols "^1.0.3"
+ object-keys "^1.1.1"
+
+object.fromentries@^2.0.7:
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65"
+ integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==
+ dependencies:
+ call-bind "^1.0.7"
+ define-properties "^1.2.1"
+ es-abstract "^1.23.2"
+ es-object-atoms "^1.0.0"
+
+object.groupby@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e"
+ integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==
+ dependencies:
+ call-bind "^1.0.7"
+ define-properties "^1.2.1"
+ es-abstract "^1.23.2"
+
+object.values@^1.1.7:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b"
+ integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==
+ dependencies:
+ call-bind "^1.0.7"
+ define-properties "^1.2.1"
+ es-object-atoms "^1.0.0"
+
+once@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+ dependencies:
+ wrappy "1"
+
+onetime@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
+ integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
+ dependencies:
+ mimic-fn "^2.1.0"
+
+optionator@^0.9.3:
+ version "0.9.4"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
+ integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==
+ dependencies:
+ deep-is "^0.1.3"
+ fast-levenshtein "^2.0.6"
+ levn "^0.4.1"
+ prelude-ls "^1.2.1"
+ type-check "^0.4.0"
+ word-wrap "^1.2.5"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+ integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
+
+p-limit@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+ integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+ dependencies:
+ p-try "^2.0.0"
+
+p-limit@^3.0.2, p-limit@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+ integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+ dependencies:
+ yocto-queue "^0.1.0"
+
+p-locate@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+ integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+ dependencies:
+ p-limit "^2.2.0"
+
+p-locate@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
+ integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+ dependencies:
+ p-limit "^3.0.2"
+
+p-queue@^6.6.2:
+ version "6.6.2"
+ resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426"
+ integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==
+ dependencies:
+ eventemitter3 "^4.0.4"
+ p-timeout "^3.2.0"
+
+p-retry@4:
+ version "4.6.2"
+ resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16"
+ integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==
+ dependencies:
+ "@types/retry" "0.12.0"
+ retry "^0.13.1"
+
+p-timeout@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
+ integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
+ dependencies:
+ p-finally "^1.0.0"
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+ integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+parent-module@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+ integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+ dependencies:
+ callsites "^3.0.0"
+
+parse-json@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
+ integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ error-ex "^1.3.1"
+ json-parse-even-better-errors "^2.3.0"
+ lines-and-columns "^1.1.6"
+
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+ integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+
+path-key@^3.0.0, path-key@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+ integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+path-type@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+ integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+picocolors@^1.0.0, picocolors@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
+ integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
+
+picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pirates@^4.0.4:
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
+ integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
+
+pkg-dir@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+ integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+ dependencies:
+ find-up "^4.0.0"
+
+possible-typed-array-names@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f"
+ integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==
+
+prelude-ls@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
+ integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
+
+prettier-linter-helpers@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
+ integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
+ dependencies:
+ fast-diff "^1.1.2"
+
+prettier@^3.3.3:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"
+ integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==
+
+pretty-format@^29.0.0, pretty-format@^29.7.0:
+ version "29.7.0"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812"
+ integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==
+ dependencies:
+ "@jest/schemas" "^29.6.3"
+ ansi-styles "^5.0.0"
+ react-is "^18.0.0"
+
+prompts@^2.0.1:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"
+ integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==
+ dependencies:
+ kleur "^3.0.3"
+ sisteransi "^1.0.5"
+
+punycode@^2.1.0:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
+ integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
+
+pure-rand@^6.0.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2"
+ integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==
+
+queue-microtask@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+ integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+react-is@^18.0.0:
+ version "18.3.1"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
+ integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
+
+regexp.prototype.flags@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334"
+ integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==
+ dependencies:
+ call-bind "^1.0.6"
+ define-properties "^1.2.1"
+ es-errors "^1.3.0"
+ set-function-name "^2.0.1"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
+
+resolve-cwd@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
+ integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
+ dependencies:
+ resolve-from "^5.0.0"
+
+resolve-from@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+ integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve-from@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+ integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
+resolve.exports@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800"
+ integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==
+
+resolve@^1.20.0, resolve@^1.22.4:
+ version "1.22.8"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
+ integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
+ dependencies:
+ is-core-module "^2.13.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
+retry@^0.13.1:
+ version "0.13.1"
+ resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
+ integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==
+
+reusify@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+ integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+rimraf@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+ integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+ dependencies:
+ glob "^7.1.3"
+
+run-parallel@^1.1.9:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+ integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+ dependencies:
+ queue-microtask "^1.2.2"
+
+safe-array-concat@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb"
+ integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==
+ dependencies:
+ call-bind "^1.0.7"
+ get-intrinsic "^1.2.4"
+ has-symbols "^1.0.3"
+ isarray "^2.0.5"
+
+safe-regex-test@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377"
+ integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==
+ dependencies:
+ call-bind "^1.0.6"
+ es-errors "^1.3.0"
+ is-regex "^1.1.4"
+
+semver@^6.3.0, semver@^6.3.1:
+ version "6.3.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
+ integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
+
+semver@^7.3.7, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3:
+ version "7.6.3"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
+ integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
+
+set-function-length@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
+ integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
+ dependencies:
+ define-data-property "^1.1.4"
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+ get-intrinsic "^1.2.4"
+ gopd "^1.0.1"
+ has-property-descriptors "^1.0.2"
+
+set-function-name@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985"
+ integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==
+ dependencies:
+ define-data-property "^1.1.4"
+ es-errors "^1.3.0"
+ functions-have-names "^1.2.3"
+ has-property-descriptors "^1.0.2"
+
+shebang-command@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+ integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+ dependencies:
+ shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+ integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+side-channel@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
+ integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
+ dependencies:
+ call-bind "^1.0.7"
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.4"
+ object-inspect "^1.13.1"
+
+signal-exit@^3.0.3, signal-exit@^3.0.7:
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
+ integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+
+sisteransi@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
+ integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
+
+slash@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+ integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+
+source-map-support@0.5.13:
+ version "0.5.13"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932"
+ integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map@^0.6.0, source-map@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+ integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
+
+stack-utils@^2.0.3:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f"
+ integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==
+ dependencies:
+ escape-string-regexp "^2.0.0"
+
+string-length@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
+ integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==
+ dependencies:
+ char-regex "^1.0.2"
+ strip-ansi "^6.0.0"
+
+string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string.prototype.trim@^1.2.9:
+ version "1.2.9"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4"
+ integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==
+ dependencies:
+ call-bind "^1.0.7"
+ define-properties "^1.2.1"
+ es-abstract "^1.23.0"
+ es-object-atoms "^1.0.0"
+
+string.prototype.trimend@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229"
+ integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==
+ dependencies:
+ call-bind "^1.0.7"
+ define-properties "^1.2.1"
+ es-object-atoms "^1.0.0"
+
+string.prototype.trimstart@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde"
+ integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==
+ dependencies:
+ call-bind "^1.0.7"
+ define-properties "^1.2.1"
+ es-object-atoms "^1.0.0"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+ integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
+
+strip-bom@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
+ integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
+
+strip-final-newline@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
+ integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
+
+strip-json-comments@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+ integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
+supports-color@^5.3.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ dependencies:
+ has-flag "^3.0.0"
+
+supports-color@^7.1.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+ integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+ dependencies:
+ has-flag "^4.0.0"
+
+supports-color@^8.0.0:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+ integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+ dependencies:
+ has-flag "^4.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+test-exclude@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
+ integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==
+ dependencies:
+ "@istanbuljs/schema" "^0.1.2"
+ glob "^7.1.4"
+ minimatch "^3.0.4"
+
+text-table@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+ integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
+
+tmpl@1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
+ integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==
+
+to-fast-properties@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+ integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+ts-jest@^29.1.0:
+ version "29.2.4"
+ resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.4.tgz#38ccf487407d7a63054a72689f6f99b075e296e5"
+ integrity sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==
+ dependencies:
+ bs-logger "0.x"
+ ejs "^3.1.10"
+ fast-json-stable-stringify "2.x"
+ jest-util "^29.0.0"
+ json5 "^2.2.3"
+ lodash.memoize "4.x"
+ make-error "1.x"
+ semver "^7.5.3"
+ yargs-parser "^21.0.1"
+
+tsconfig-paths@^3.15.0:
+ version "3.15.0"
+ resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4"
+ integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==
+ dependencies:
+ "@types/json5" "^0.0.29"
+ json5 "^1.0.2"
+ minimist "^1.2.6"
+ strip-bom "^3.0.0"
+
+tslib@^1.8.1:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+ integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
+tsutils@^3.21.0:
+ version "3.21.0"
+ resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
+ integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
+ dependencies:
+ tslib "^1.8.1"
+
+type-check@^0.4.0, type-check@~0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
+ integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
+ dependencies:
+ prelude-ls "^1.2.1"
+
+type-detect@4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+ integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+
+type-fest@^0.20.2:
+ version "0.20.2"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
+ integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
+
+type-fest@^0.21.3:
+ version "0.21.3"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
+ integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
+
+typed-array-buffer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3"
+ integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==
+ dependencies:
+ call-bind "^1.0.7"
+ es-errors "^1.3.0"
+ is-typed-array "^1.1.13"
+
+typed-array-byte-length@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67"
+ integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==
+ dependencies:
+ call-bind "^1.0.7"
+ for-each "^0.3.3"
+ gopd "^1.0.1"
+ has-proto "^1.0.3"
+ is-typed-array "^1.1.13"
+
+typed-array-byte-offset@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063"
+ integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==
+ dependencies:
+ available-typed-arrays "^1.0.7"
+ call-bind "^1.0.7"
+ for-each "^0.3.3"
+ gopd "^1.0.1"
+ has-proto "^1.0.3"
+ is-typed-array "^1.1.13"
+
+typed-array-length@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3"
+ integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==
+ dependencies:
+ call-bind "^1.0.7"
+ for-each "^0.3.3"
+ gopd "^1.0.1"
+ has-proto "^1.0.3"
+ is-typed-array "^1.1.13"
+ possible-typed-array-names "^1.0.0"
+
+typescript@^5.3.3:
+ version "5.5.4"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
+ integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
+
+unbox-primitive@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
+ integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==
+ dependencies:
+ call-bind "^1.0.2"
+ has-bigints "^1.0.2"
+ has-symbols "^1.0.3"
+ which-boxed-primitive "^1.0.2"
+
+undici-types@~6.19.2:
+ version "6.19.8"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
+ integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
+
+update-browserslist-db@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e"
+ integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==
+ dependencies:
+ escalade "^3.1.2"
+ picocolors "^1.0.1"
+
+uri-js@^4.2.2:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+ integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+ dependencies:
+ punycode "^2.1.0"
+
+uuid@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
+ integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
+
+uuid@^9.0.0:
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
+ integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
+
+v8-to-istanbul@^9.0.1:
+ version "9.3.0"
+ resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175"
+ integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==
+ dependencies:
+ "@jridgewell/trace-mapping" "^0.3.12"
+ "@types/istanbul-lib-coverage" "^2.0.1"
+ convert-source-map "^2.0.0"
+
+walker@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f"
+ integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==
+ dependencies:
+ makeerror "1.0.12"
+
+which-boxed-primitive@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
+ integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
+ dependencies:
+ is-bigint "^1.0.1"
+ is-boolean-object "^1.1.0"
+ is-number-object "^1.0.4"
+ is-string "^1.0.5"
+ is-symbol "^1.0.3"
+
+which-typed-array@^1.1.14, which-typed-array@^1.1.15:
+ version "1.1.15"
+ resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d"
+ integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==
+ dependencies:
+ available-typed-arrays "^1.0.7"
+ call-bind "^1.0.7"
+ for-each "^0.3.3"
+ gopd "^1.0.1"
+ has-tostringtag "^1.0.2"
+
+which@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ dependencies:
+ isexe "^2.0.0"
+
+word-wrap@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
+ integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
+
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+write-file-atomic@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd"
+ integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==
+ dependencies:
+ imurmurhash "^0.1.4"
+ signal-exit "^3.0.7"
+
+y18n@^5.0.5:
+ version "5.0.8"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
+ integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+
+yallist@^3.0.2:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
+ integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+
+yargs-parser@^21.0.1, yargs-parser@^21.1.1:
+ version "21.1.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
+ integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
+
+yargs@^17.3.1:
+ version "17.7.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
+ integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
+ dependencies:
+ cliui "^8.0.1"
+ escalade "^3.1.1"
+ get-caller-file "^2.0.5"
+ require-directory "^2.1.1"
+ string-width "^4.2.3"
+ y18n "^5.0.5"
+ yargs-parser "^21.1.1"
+
+yocto-queue@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+ integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+
+zod-to-json-schema@^3.22.3:
+ version "3.23.2"
+ resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.23.2.tgz#bc7e379c8050462538383e382964c03d8fe008f9"
+ integrity sha512-uSt90Gzc/tUfyNqxnjlfBs8W6WSGpNBv0rVsNxP/BVSMHMKGdthPYff4xtCHYloJGM0CFxFsb3NbC0eqPhfImw==
+
+zod@^3.22.4:
+ version "3.23.8"
+ resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
+ integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
+
+zod@^3.23.8:
+ version "3.24.3"
+ resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.3.tgz#1f40f750a05e477396da64438e0e1c0995dafd87"
+ integrity sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==
diff --git a/langgraph/source/libs/cli/js-monorepo-example/.eslintrc.cjs b/langgraph/source/libs/cli/js-monorepo-example/.eslintrc.cjs
new file mode 100644
index 0000000000000000000000000000000000000000..da4c3ecb471bcd309ae38967e5e24cbb5f66ee23
--- /dev/null
+++ b/langgraph/source/libs/cli/js-monorepo-example/.eslintrc.cjs
@@ -0,0 +1,62 @@
+module.exports = {
+ extends: [
+ "eslint:recommended",
+ "prettier",
+ "plugin:@typescript-eslint/recommended",
+ ],
+ parserOptions: {
+ ecmaVersion: 12,
+ parser: "@typescript-eslint/parser",
+ project: "./tsconfig.json",
+ sourceType: "module",
+ },
+ plugins: ["import", "@typescript-eslint", "no-instanceof"],
+ ignorePatterns: [
+ ".eslintrc.cjs",
+ "scripts",
+ "src/utils/lodash/*",
+ "node_modules",
+ "dist",
+ "dist-cjs",
+ "*.js",
+ "*.cjs",
+ "*.d.ts",
+ ],
+ rules: {
+ "no-process-env": 2,
+ "no-instanceof/no-instanceof": 2,
+ "@typescript-eslint/explicit-module-boundary-types": 0,
+ "@typescript-eslint/no-empty-function": 0,
+ "@typescript-eslint/no-shadow": 0,
+ "@typescript-eslint/no-empty-interface": 0,
+ "@typescript-eslint/no-use-before-define": ["error", "nofunc"],
+ "@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
+ "@typescript-eslint/no-floating-promises": "error",
+ "@typescript-eslint/no-misused-promises": "error",
+ camelcase: 0,
+ "class-methods-use-this": 0,
+ "import/extensions": [2, "ignorePackages"],
+ "import/no-extraneous-dependencies": [
+ "error",
+ { devDependencies: ["**/*.test.ts"] },
+ ],
+ "import/no-unresolved": 0,
+ "import/prefer-default-export": 0,
+ "keyword-spacing": "error",
+ "max-classes-per-file": 0,
+ "max-len": 0,
+ "no-await-in-loop": 0,
+ "no-bitwise": 0,
+ "no-console": 0,
+ "no-restricted-syntax": 0,
+ "no-shadow": 0,
+ "no-continue": 0,
+ "no-underscore-dangle": 0,
+ "no-use-before-define": 0,
+ "no-useless-constructor": 0,
+ "no-return-await": 0,
+ "consistent-return": 0,
+ "no-else-return": 0,
+ "new-cap": ["error", { properties: false, capIsNew: false }],
+ },
+};
diff --git a/langgraph/source/libs/cli/js-monorepo-example/apps/agent/langgraph.json b/langgraph/source/libs/cli/js-monorepo-example/apps/agent/langgraph.json
new file mode 100644
index 0000000000000000000000000000000000000000..e39ef7891e2d1506cf441019de1467435971c1dc
--- /dev/null
+++ b/langgraph/source/libs/cli/js-monorepo-example/apps/agent/langgraph.json
@@ -0,0 +1,7 @@
+{
+ "node_version": "20",
+ "graphs": {
+ "agent": "./src/graph.ts:graph"
+ },
+ "env": "../../.env"
+}
diff --git a/langgraph/source/libs/cli/js-monorepo-example/apps/agent/package.json b/langgraph/source/libs/cli/js-monorepo-example/apps/agent/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..e55e3b3387d05e8875602d06a97bd981a4ca8d67
--- /dev/null
+++ b/langgraph/source/libs/cli/js-monorepo-example/apps/agent/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@js-monorepo-example/agent",
+ "version": "0.0.1",
+ "type": "module",
+ "main": "src/graph.ts",
+ "scripts": {
+ "build": "tsc",
+ "clean": "rm -rf dist"
+ },
+ "dependencies": {
+ "@js-monorepo-example/shared": "*",
+ "@langchain/core": "^0.3.2",
+ "@langchain/langgraph": "^0.2.5"
+ },
+ "devDependencies": {
+ "typescript": "^5.3.3"
+ }
+}
diff --git a/langgraph/source/libs/cli/js-monorepo-example/apps/agent/src/graph.ts b/langgraph/source/libs/cli/js-monorepo-example/apps/agent/src/graph.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6bc4e6311ef2967849228426d1519fdbd204e686
--- /dev/null
+++ b/langgraph/source/libs/cli/js-monorepo-example/apps/agent/src/graph.ts
@@ -0,0 +1,47 @@
+/**
+ * Simple LangGraph.js example for monorepo testing
+ */
+import { StateGraph } from "@langchain/langgraph";
+import { RunnableConfig } from "@langchain/core/runnables";
+import { StateAnnotation } from "./state.js";
+import { getGreeting } from "@js-monorepo-example/shared";
+
+/**
+ * Simple node that uses the shared library
+ */
+const callModel = async (
+ state: typeof StateAnnotation.State,
+ _config: RunnableConfig,
+): Promise => {
+ // Use functions from the shared library
+ const greeting = getGreeting();
+
+ return {
+ messages: [
+ {
+ role: "assistant",
+ content: `${greeting}`,
+ },
+ ],
+ };
+};
+
+/**
+ * Simple routing function
+ */
+export const route = (
+ state: typeof StateAnnotation.State,
+): "__end__" | "callModel" => {
+ if (state.messages.length > 0) {
+ return "__end__";
+ }
+ return "callModel";
+};
+
+// Create the graph
+const builder = new StateGraph(StateAnnotation)
+ .addNode("callModel", callModel)
+ .addEdge("__start__", "callModel")
+ .addConditionalEdges("callModel", route);
+
+export const graph = builder.compile();
diff --git a/langgraph/source/libs/cli/js-monorepo-example/apps/agent/src/state.ts b/langgraph/source/libs/cli/js-monorepo-example/apps/agent/src/state.ts
new file mode 100644
index 0000000000000000000000000000000000000000..eeeecd4172bff311bff02c92f05ce87319033d80
--- /dev/null
+++ b/langgraph/source/libs/cli/js-monorepo-example/apps/agent/src/state.ts
@@ -0,0 +1,15 @@
+import { BaseMessage, BaseMessageLike } from "@langchain/core/messages";
+import { Annotation, messagesStateReducer } from "@langchain/langgraph";
+
+/**
+ * Simple state annotation for the agent
+ */
+export const StateAnnotation = Annotation.Root({
+ /**
+ * Messages track the primary execution state of the agent.
+ */
+ messages: Annotation({
+ reducer: messagesStateReducer,
+ default: () => [],
+ }),
+});
diff --git a/langgraph/source/libs/cli/js-monorepo-example/apps/agent/tsconfig.json b/langgraph/source/libs/cli/js-monorepo-example/apps/agent/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..ed464a96bfffe451987adb9723928a45a9f23da9
--- /dev/null
+++ b/langgraph/source/libs/cli/js-monorepo-example/apps/agent/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/langgraph/source/libs/cli/js-monorepo-example/libs/shared/package.json b/langgraph/source/libs/cli/js-monorepo-example/libs/shared/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..d220ae394af9c9bef38524f65f360d0b92f82ba0
--- /dev/null
+++ b/langgraph/source/libs/cli/js-monorepo-example/libs/shared/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@js-monorepo-example/shared",
+ "version": "0.0.1",
+ "type": "module",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "scripts": {
+ "build": "tsc",
+ "clean": "rm -rf dist"
+ },
+ "devDependencies": {
+ "typescript": "^5.3.3"
+ }
+}
diff --git a/langgraph/source/libs/cli/js-monorepo-example/libs/shared/src/index.ts b/langgraph/source/libs/cli/js-monorepo-example/libs/shared/src/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..19878933cad4771aca838a87233c4cc3d5175ab6
--- /dev/null
+++ b/langgraph/source/libs/cli/js-monorepo-example/libs/shared/src/index.ts
@@ -0,0 +1,6 @@
+/**
+ * Simple utility functions for monorepo testing
+ */
+export function getGreeting(): string {
+ return "Hello from shared library!";
+}
diff --git a/langgraph/source/libs/cli/js-monorepo-example/libs/shared/tsconfig.json b/langgraph/source/libs/cli/js-monorepo-example/libs/shared/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..ed464a96bfffe451987adb9723928a45a9f23da9
--- /dev/null
+++ b/langgraph/source/libs/cli/js-monorepo-example/libs/shared/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/langgraph/source/libs/cli/js-monorepo-example/package.json b/langgraph/source/libs/cli/js-monorepo-example/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..22eb6b37d3f0b9439005f8d6092bc03ac7a4d4ed
--- /dev/null
+++ b/langgraph/source/libs/cli/js-monorepo-example/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "js-monorepo-example",
+ "version": "0.0.1",
+ "packageManager": "yarn@1.22.22",
+ "description": "A simple monorepo example for LangGraph integration testing.",
+ "private": true,
+ "workspaces": [
+ "libs/*",
+ "apps/*"
+ ],
+ "type": "module",
+ "scripts": {
+ "build": "turbo build",
+ "clean": "turbo clean",
+ "test": "turbo test",
+ "format": "prettier --write .",
+ "lint": "eslint 'apps/**/*.ts' 'libs/**/*.ts'"
+ },
+ "devDependencies": {
+ "turbo": "^2.5.0",
+ "typescript": "^5.3.3",
+ "@tsconfig/recommended": "^1.0.7",
+ "@eslint/eslintrc": "^3.1.0",
+ "@eslint/js": "^9.9.1",
+ "eslint": "^8.41.0",
+ "eslint-config-prettier": "^8.8.0",
+ "eslint-plugin-import": "^2.27.5",
+ "eslint-plugin-no-instanceof": "^1.0.1",
+ "eslint-plugin-prettier": "^4.2.1",
+ "@typescript-eslint/eslint-plugin": "^5.59.8",
+ "@typescript-eslint/parser": "^5.59.8",
+ "prettier": "^3.3.3"
+ }
+}
diff --git a/langgraph/source/libs/cli/js-monorepo-example/tsconfig.json b/langgraph/source/libs/cli/js-monorepo-example/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..3aa3da2d9daae20b3df18152326b803a172b6225
--- /dev/null
+++ b/langgraph/source/libs/cli/js-monorepo-example/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "@tsconfig/recommended",
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "declaration": true,
+ "outDir": "./dist"
+ },
+ "include": ["apps/**/*", "libs/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/langgraph/source/libs/cli/js-monorepo-example/turbo.json b/langgraph/source/libs/cli/js-monorepo-example/turbo.json
new file mode 100644
index 0000000000000000000000000000000000000000..815403faacf5df55a793f73a86639ef34bc764c3
--- /dev/null
+++ b/langgraph/source/libs/cli/js-monorepo-example/turbo.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "tasks": {
+ "build": {
+ "dependsOn": ["^build"],
+ "outputs": ["dist/**"]
+ },
+ "clean": {
+ "dependsOn": ["^clean"]
+ },
+ "test": {
+ "dependsOn": ["^test"]
+ }
+ }
+}
diff --git a/langgraph/source/libs/cli/js-monorepo-example/yarn.lock b/langgraph/source/libs/cli/js-monorepo-example/yarn.lock
new file mode 100644
index 0000000000000000000000000000000000000000..29246958911cf6de3f03b3db6c13507274d385ec
--- /dev/null
+++ b/langgraph/source/libs/cli/js-monorepo-example/yarn.lock
@@ -0,0 +1,2204 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@cfworker/json-schema@^4.0.2":
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/@cfworker/json-schema/-/json-schema-4.1.1.tgz#4a2a3947ee9fa7b7c24be981422831b8674c3be6"
+ integrity sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==
+
+"@eslint-community/eslint-utils@^4.2.0":
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"
+ integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==
+ dependencies:
+ eslint-visitor-keys "^3.4.3"
+
+"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1":
+ version "4.12.1"
+ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
+ integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
+
+"@eslint/eslintrc@^2.1.4":
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad"
+ integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==
+ dependencies:
+ ajv "^6.12.4"
+ debug "^4.3.2"
+ espree "^9.6.0"
+ globals "^13.19.0"
+ ignore "^5.2.0"
+ import-fresh "^3.2.1"
+ js-yaml "^4.1.0"
+ minimatch "^3.1.2"
+ strip-json-comments "^3.1.1"
+
+"@eslint/eslintrc@^3.1.0":
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz#e55f7f1dd400600dd066dbba349c4c0bac916964"
+ integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==
+ dependencies:
+ ajv "^6.12.4"
+ debug "^4.3.2"
+ espree "^10.0.1"
+ globals "^14.0.0"
+ ignore "^5.2.0"
+ import-fresh "^3.2.1"
+ js-yaml "^4.1.0"
+ minimatch "^3.1.2"
+ strip-json-comments "^3.1.1"
+
+"@eslint/js@8.57.1":
+ version "8.57.1"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2"
+ integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
+
+"@eslint/js@^9.9.1":
+ version "9.34.0"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.34.0.tgz#fc423168b9d10e08dea9088d083788ec6442996b"
+ integrity sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==
+
+"@humanwhocodes/config-array@^0.13.0":
+ version "0.13.0"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748"
+ integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==
+ dependencies:
+ "@humanwhocodes/object-schema" "^2.0.3"
+ debug "^4.3.1"
+ minimatch "^3.0.5"
+
+"@humanwhocodes/module-importer@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
+ integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
+
+"@humanwhocodes/object-schema@^2.0.3":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
+ integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
+
+"@langchain/core@^0.3.2":
+ version "0.3.72"
+ resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.3.72.tgz#725e2fc863c45672862c8486083e5703557ab422"
+ integrity sha512-WsGWVZYnlKffj2eEfDocPNiaTRoxyYiLSQdQ7oxZvxGZBqo/90vpjbC33UGK1uPNBM4kT+pkdaol/MnvKUh8TQ==
+ dependencies:
+ "@cfworker/json-schema" "^4.0.2"
+ ansi-styles "^5.0.0"
+ camelcase "6"
+ decamelize "1.2.0"
+ js-tiktoken "^1.0.12"
+ langsmith "^0.3.46"
+ mustache "^4.2.0"
+ p-queue "^6.6.2"
+ p-retry "4"
+ uuid "^10.0.0"
+ zod "^3.25.32"
+ zod-to-json-schema "^3.22.3"
+
+"@langchain/langgraph-checkpoint@~0.0.17":
+ version "0.0.18"
+ resolved "https://registry.yarnpkg.com/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.0.18.tgz#2f7a9cdeda948ccc8d312ba9463810709d71d0b8"
+ integrity sha512-IS7zJj36VgY+4pf8ZjsVuUWef7oTwt1y9ylvwu0aLuOn1d0fg05Om9DLm3v2GZ2Df6bhLV1kfWAM0IAl9O5rQQ==
+ dependencies:
+ uuid "^10.0.0"
+
+"@langchain/langgraph-sdk@~0.0.32":
+ version "0.0.112"
+ resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.112.tgz#3186919b60e3381aa8aa32ea9b9c39df1f02a9fd"
+ integrity sha512-/9W5HSWCqYgwma6EoOspL4BGYxGxeJP6lIquPSF4FA0JlKopaUv58ucZC3vAgdJyCgg6sorCIV/qg7SGpEcCLw==
+ dependencies:
+ "@types/json-schema" "^7.0.15"
+ p-queue "^6.6.2"
+ p-retry "4"
+ uuid "^9.0.0"
+
+"@langchain/langgraph@^0.2.5":
+ version "0.2.74"
+ resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-0.2.74.tgz#37367a1e8bafda3548037a91449a69a84f285def"
+ integrity sha512-oHpEi5sTZTPaeZX1UnzfM2OAJ21QGQrwReTV6+QnX7h8nDCBzhtipAw1cK616S+X8zpcVOjgOtJuaJhXa4mN8w==
+ dependencies:
+ "@langchain/langgraph-checkpoint" "~0.0.17"
+ "@langchain/langgraph-sdk" "~0.0.32"
+ uuid "^10.0.0"
+ zod "^3.23.8"
+
+"@nodelib/fs.scandir@2.1.5":
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+ integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+ dependencies:
+ "@nodelib/fs.stat" "2.0.5"
+ run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+ integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+ dependencies:
+ "@nodelib/fs.scandir" "2.1.5"
+ fastq "^1.6.0"
+
+"@rtsao/scc@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
+ integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==
+
+"@tsconfig/recommended@^1.0.7":
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/@tsconfig/recommended/-/recommended-1.0.10.tgz#5ed23fcf8cca7d78a9e3a6e4828cd96cf783994c"
+ integrity sha512-cGvydvg03lONp5Z9yaplW493Vw9/um7k588mvDkm+VFPF2PZUVPx0uswq4PFpeEySsLbQRETrDRhzh4Dmxaslw==
+
+"@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9":
+ version "7.0.15"
+ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
+ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
+
+"@types/json5@^0.0.29":
+ version "0.0.29"
+ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
+ integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
+
+"@types/retry@0.12.0":
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
+ integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
+
+"@types/semver@^7.3.12":
+ version "7.7.0"
+ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.0.tgz#64c441bdae033b378b6eef7d0c3d77c329b9378e"
+ integrity sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==
+
+"@types/uuid@^10.0.0":
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d"
+ integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==
+
+"@typescript-eslint/eslint-plugin@^5.59.8":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db"
+ integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==
+ dependencies:
+ "@eslint-community/regexpp" "^4.4.0"
+ "@typescript-eslint/scope-manager" "5.62.0"
+ "@typescript-eslint/type-utils" "5.62.0"
+ "@typescript-eslint/utils" "5.62.0"
+ debug "^4.3.4"
+ graphemer "^1.4.0"
+ ignore "^5.2.0"
+ natural-compare-lite "^1.4.0"
+ semver "^7.3.7"
+ tsutils "^3.21.0"
+
+"@typescript-eslint/parser@^5.59.8":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7"
+ integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==
+ dependencies:
+ "@typescript-eslint/scope-manager" "5.62.0"
+ "@typescript-eslint/types" "5.62.0"
+ "@typescript-eslint/typescript-estree" "5.62.0"
+ debug "^4.3.4"
+
+"@typescript-eslint/scope-manager@5.62.0":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c"
+ integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==
+ dependencies:
+ "@typescript-eslint/types" "5.62.0"
+ "@typescript-eslint/visitor-keys" "5.62.0"
+
+"@typescript-eslint/type-utils@5.62.0":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a"
+ integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==
+ dependencies:
+ "@typescript-eslint/typescript-estree" "5.62.0"
+ "@typescript-eslint/utils" "5.62.0"
+ debug "^4.3.4"
+ tsutils "^3.21.0"
+
+"@typescript-eslint/types@5.62.0":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f"
+ integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==
+
+"@typescript-eslint/typescript-estree@5.62.0":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b"
+ integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==
+ dependencies:
+ "@typescript-eslint/types" "5.62.0"
+ "@typescript-eslint/visitor-keys" "5.62.0"
+ debug "^4.3.4"
+ globby "^11.1.0"
+ is-glob "^4.0.3"
+ semver "^7.3.7"
+ tsutils "^3.21.0"
+
+"@typescript-eslint/utils@5.62.0":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86"
+ integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==
+ dependencies:
+ "@eslint-community/eslint-utils" "^4.2.0"
+ "@types/json-schema" "^7.0.9"
+ "@types/semver" "^7.3.12"
+ "@typescript-eslint/scope-manager" "5.62.0"
+ "@typescript-eslint/types" "5.62.0"
+ "@typescript-eslint/typescript-estree" "5.62.0"
+ eslint-scope "^5.1.1"
+ semver "^7.3.7"
+
+"@typescript-eslint/visitor-keys@5.62.0":
+ version "5.62.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e"
+ integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==
+ dependencies:
+ "@typescript-eslint/types" "5.62.0"
+ eslint-visitor-keys "^3.3.0"
+
+"@ungap/structured-clone@^1.2.0":
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8"
+ integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
+
+acorn-jsx@^5.3.2:
+ version "5.3.2"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
+ integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
+
+acorn@^8.15.0, acorn@^8.9.0:
+ version "8.15.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
+ integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
+
+ajv@^6.12.4:
+ version "6.12.6"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+ integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+ansi-regex@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+ integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+ integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+ dependencies:
+ color-convert "^2.0.1"
+
+ansi-styles@^5.0.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
+ integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
+
+argparse@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+ integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b"
+ integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==
+ dependencies:
+ call-bound "^1.0.3"
+ is-array-buffer "^3.0.5"
+
+array-includes@^3.1.9:
+ version "3.1.9"
+ resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.9.tgz#1f0ccaa08e90cdbc3eb433210f903ad0f17c3f3a"
+ integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==
+ dependencies:
+ call-bind "^1.0.8"
+ call-bound "^1.0.4"
+ define-properties "^1.2.1"
+ es-abstract "^1.24.0"
+ es-object-atoms "^1.1.1"
+ get-intrinsic "^1.3.0"
+ is-string "^1.1.1"
+ math-intrinsics "^1.1.0"
+
+array-union@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
+ integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+
+array.prototype.findlastindex@^1.2.6:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz#cfa1065c81dcb64e34557c9b81d012f6a421c564"
+ integrity sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==
+ dependencies:
+ call-bind "^1.0.8"
+ call-bound "^1.0.4"
+ define-properties "^1.2.1"
+ es-abstract "^1.23.9"
+ es-errors "^1.3.0"
+ es-object-atoms "^1.1.1"
+ es-shim-unscopables "^1.1.0"
+
+array.prototype.flat@^1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5"
+ integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==
+ dependencies:
+ call-bind "^1.0.8"
+ define-properties "^1.2.1"
+ es-abstract "^1.23.5"
+ es-shim-unscopables "^1.0.2"
+
+array.prototype.flatmap@^1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz#712cc792ae70370ae40586264629e33aab5dd38b"
+ integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==
+ dependencies:
+ call-bind "^1.0.8"
+ define-properties "^1.2.1"
+ es-abstract "^1.23.5"
+ es-shim-unscopables "^1.0.2"
+
+arraybuffer.prototype.slice@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c"
+ integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==
+ dependencies:
+ array-buffer-byte-length "^1.0.1"
+ call-bind "^1.0.8"
+ define-properties "^1.2.1"
+ es-abstract "^1.23.5"
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.6"
+ is-array-buffer "^3.0.4"
+
+async-function@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b"
+ integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==
+
+available-typed-arrays@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
+ integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==
+ dependencies:
+ possible-typed-array-names "^1.0.0"
+
+balanced-match@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+base64-js@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+ integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
+brace-expansion@^1.1.7:
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
+ integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+ integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
+ dependencies:
+ fill-range "^7.1.1"
+
+call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
+ integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
+ dependencies:
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+
+call-bind@^1.0.7, call-bind@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c"
+ integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==
+ dependencies:
+ call-bind-apply-helpers "^1.0.0"
+ es-define-property "^1.0.0"
+ get-intrinsic "^1.2.4"
+ set-function-length "^1.2.2"
+
+call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
+ integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
+ dependencies:
+ call-bind-apply-helpers "^1.0.2"
+ get-intrinsic "^1.3.0"
+
+callsites@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+ integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+camelcase@6:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
+ integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
+
+chalk@^4.0.0, chalk@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+ integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+ dependencies:
+ ansi-styles "^4.1.0"
+ supports-color "^7.1.0"
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
+color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+console-table-printer@^2.12.1:
+ version "2.14.6"
+ resolved "https://registry.yarnpkg.com/console-table-printer/-/console-table-printer-2.14.6.tgz#edfe0bf311fa2701922ed509443145ab51e06436"
+ integrity sha512-MCBl5HNVaFuuHW6FGbL/4fB7N/ormCy+tQ+sxTrF6QtSbSNETvPuOVbkJBhzDgYhvjWGrTma4eYJa37ZuoQsPw==
+ dependencies:
+ simple-wcswidth "^1.0.1"
+
+cross-spawn@^7.0.2:
+ 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"
+ which "^2.0.1"
+
+data-view-buffer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570"
+ integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==
+ dependencies:
+ call-bound "^1.0.3"
+ es-errors "^1.3.0"
+ is-data-view "^1.0.2"
+
+data-view-byte-length@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735"
+ integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==
+ dependencies:
+ call-bound "^1.0.3"
+ es-errors "^1.3.0"
+ is-data-view "^1.0.2"
+
+data-view-byte-offset@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191"
+ integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==
+ dependencies:
+ call-bound "^1.0.2"
+ es-errors "^1.3.0"
+ is-data-view "^1.0.1"
+
+debug@^3.2.7:
+ version "3.2.7"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+ integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
+ dependencies:
+ ms "^2.1.1"
+
+debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
+ integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
+ dependencies:
+ ms "^2.1.3"
+
+decamelize@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+ integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
+
+deep-is@^0.1.3:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
+ integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
+
+define-data-property@^1.0.1, define-data-property@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
+ integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
+ dependencies:
+ es-define-property "^1.0.0"
+ es-errors "^1.3.0"
+ gopd "^1.0.1"
+
+define-properties@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c"
+ integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==
+ dependencies:
+ define-data-property "^1.0.1"
+ has-property-descriptors "^1.0.0"
+ object-keys "^1.1.1"
+
+dir-glob@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
+ integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
+ dependencies:
+ path-type "^4.0.0"
+
+doctrine@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
+ integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==
+ dependencies:
+ esutils "^2.0.2"
+
+doctrine@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
+ integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
+ dependencies:
+ esutils "^2.0.2"
+
+dunder-proto@^1.0.0, dunder-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
+ integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
+ dependencies:
+ call-bind-apply-helpers "^1.0.1"
+ es-errors "^1.3.0"
+ gopd "^1.2.0"
+
+es-abstract@^1.23.2, es-abstract@^1.23.5, es-abstract@^1.23.9, es-abstract@^1.24.0:
+ version "1.24.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.0.tgz#c44732d2beb0acc1ed60df840869e3106e7af328"
+ integrity sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==
+ dependencies:
+ array-buffer-byte-length "^1.0.2"
+ arraybuffer.prototype.slice "^1.0.4"
+ available-typed-arrays "^1.0.7"
+ call-bind "^1.0.8"
+ call-bound "^1.0.4"
+ data-view-buffer "^1.0.2"
+ data-view-byte-length "^1.0.2"
+ data-view-byte-offset "^1.0.1"
+ es-define-property "^1.0.1"
+ es-errors "^1.3.0"
+ es-object-atoms "^1.1.1"
+ es-set-tostringtag "^2.1.0"
+ es-to-primitive "^1.3.0"
+ function.prototype.name "^1.1.8"
+ get-intrinsic "^1.3.0"
+ get-proto "^1.0.1"
+ get-symbol-description "^1.1.0"
+ globalthis "^1.0.4"
+ gopd "^1.2.0"
+ has-property-descriptors "^1.0.2"
+ has-proto "^1.2.0"
+ has-symbols "^1.1.0"
+ hasown "^2.0.2"
+ internal-slot "^1.1.0"
+ is-array-buffer "^3.0.5"
+ is-callable "^1.2.7"
+ is-data-view "^1.0.2"
+ is-negative-zero "^2.0.3"
+ is-regex "^1.2.1"
+ is-set "^2.0.3"
+ is-shared-array-buffer "^1.0.4"
+ is-string "^1.1.1"
+ is-typed-array "^1.1.15"
+ is-weakref "^1.1.1"
+ math-intrinsics "^1.1.0"
+ object-inspect "^1.13.4"
+ object-keys "^1.1.1"
+ object.assign "^4.1.7"
+ own-keys "^1.0.1"
+ regexp.prototype.flags "^1.5.4"
+ safe-array-concat "^1.1.3"
+ safe-push-apply "^1.0.0"
+ safe-regex-test "^1.1.0"
+ set-proto "^1.0.0"
+ stop-iteration-iterator "^1.1.0"
+ string.prototype.trim "^1.2.10"
+ string.prototype.trimend "^1.0.9"
+ string.prototype.trimstart "^1.0.8"
+ typed-array-buffer "^1.0.3"
+ typed-array-byte-length "^1.0.3"
+ typed-array-byte-offset "^1.0.4"
+ typed-array-length "^1.0.7"
+ unbox-primitive "^1.1.0"
+ which-typed-array "^1.1.19"
+
+es-define-property@^1.0.0, es-define-property@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
+ integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
+
+es-errors@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+ integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
+es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
+ integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
+ dependencies:
+ es-errors "^1.3.0"
+
+es-set-tostringtag@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
+ integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
+ dependencies:
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.6"
+ has-tostringtag "^1.0.2"
+ hasown "^2.0.2"
+
+es-shim-unscopables@^1.0.2, es-shim-unscopables@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz#438df35520dac5d105f3943d927549ea3b00f4b5"
+ integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==
+ dependencies:
+ hasown "^2.0.2"
+
+es-to-primitive@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18"
+ integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==
+ dependencies:
+ is-callable "^1.2.7"
+ is-date-object "^1.0.5"
+ is-symbol "^1.0.4"
+
+escape-string-regexp@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+ integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+eslint-config-prettier@^8.8.0:
+ version "8.10.2"
+ resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz#0642e53625ebc62c31c24726b0f050df6bd97a2e"
+ integrity sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==
+
+eslint-import-resolver-node@^0.3.9:
+ version "0.3.9"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac"
+ integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==
+ dependencies:
+ debug "^3.2.7"
+ is-core-module "^2.13.0"
+ resolve "^1.22.4"
+
+eslint-module-utils@^2.12.1:
+ version "2.12.1"
+ resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz#f76d3220bfb83c057651359295ab5854eaad75ff"
+ integrity sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==
+ dependencies:
+ debug "^3.2.7"
+
+eslint-plugin-import@^2.27.5:
+ version "2.32.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz#602b55faa6e4caeaa5e970c198b5c00a37708980"
+ integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==
+ dependencies:
+ "@rtsao/scc" "^1.1.0"
+ array-includes "^3.1.9"
+ array.prototype.findlastindex "^1.2.6"
+ array.prototype.flat "^1.3.3"
+ array.prototype.flatmap "^1.3.3"
+ debug "^3.2.7"
+ doctrine "^2.1.0"
+ eslint-import-resolver-node "^0.3.9"
+ eslint-module-utils "^2.12.1"
+ hasown "^2.0.2"
+ is-core-module "^2.16.1"
+ is-glob "^4.0.3"
+ minimatch "^3.1.2"
+ object.fromentries "^2.0.8"
+ object.groupby "^1.0.3"
+ object.values "^1.2.1"
+ semver "^6.3.1"
+ string.prototype.trimend "^1.0.9"
+ tsconfig-paths "^3.15.0"
+
+eslint-plugin-no-instanceof@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-no-instanceof/-/eslint-plugin-no-instanceof-1.0.1.tgz#5d9fc86d160df6991b654b294a62390207f1bb97"
+ integrity sha512-zlqQ7EsfzbRO68uI+p8FIE7zYB4njs+nNbkNjSb5QmLi2et67zQLqSeaao5U9SpnlZTTJC87nS2oyHo2ACtajw==
+
+eslint-plugin-prettier@^4.2.1:
+ version "4.2.5"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.5.tgz#91ca3f2f01a84f1272cce04e9717550494c0fe06"
+ integrity sha512-9Ni+xgemM2IWLq6aXEpP2+V/V30GeA/46Ar629vcMqVPodFFWC9skHu/D1phvuqtS8bJCFnNf01/qcmqYEwNfg==
+ dependencies:
+ prettier-linter-helpers "^1.0.0"
+
+eslint-scope@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+ integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
+ dependencies:
+ esrecurse "^4.3.0"
+ estraverse "^4.1.1"
+
+eslint-scope@^7.2.2:
+ version "7.2.2"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
+ integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
+ dependencies:
+ esrecurse "^4.3.0"
+ estraverse "^5.2.0"
+
+eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
+ version "3.4.3"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
+ integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
+
+eslint-visitor-keys@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
+ integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
+
+eslint@^8.41.0:
+ version "8.57.1"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9"
+ integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==
+ dependencies:
+ "@eslint-community/eslint-utils" "^4.2.0"
+ "@eslint-community/regexpp" "^4.6.1"
+ "@eslint/eslintrc" "^2.1.4"
+ "@eslint/js" "8.57.1"
+ "@humanwhocodes/config-array" "^0.13.0"
+ "@humanwhocodes/module-importer" "^1.0.1"
+ "@nodelib/fs.walk" "^1.2.8"
+ "@ungap/structured-clone" "^1.2.0"
+ ajv "^6.12.4"
+ chalk "^4.0.0"
+ cross-spawn "^7.0.2"
+ debug "^4.3.2"
+ doctrine "^3.0.0"
+ escape-string-regexp "^4.0.0"
+ eslint-scope "^7.2.2"
+ eslint-visitor-keys "^3.4.3"
+ espree "^9.6.1"
+ esquery "^1.4.2"
+ esutils "^2.0.2"
+ fast-deep-equal "^3.1.3"
+ file-entry-cache "^6.0.1"
+ find-up "^5.0.0"
+ glob-parent "^6.0.2"
+ globals "^13.19.0"
+ graphemer "^1.4.0"
+ ignore "^5.2.0"
+ imurmurhash "^0.1.4"
+ is-glob "^4.0.0"
+ is-path-inside "^3.0.3"
+ js-yaml "^4.1.0"
+ json-stable-stringify-without-jsonify "^1.0.1"
+ levn "^0.4.1"
+ lodash.merge "^4.6.2"
+ minimatch "^3.1.2"
+ natural-compare "^1.4.0"
+ optionator "^0.9.3"
+ strip-ansi "^6.0.1"
+ text-table "^0.2.0"
+
+espree@^10.0.1:
+ version "10.4.0"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837"
+ integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==
+ dependencies:
+ acorn "^8.15.0"
+ acorn-jsx "^5.3.2"
+ eslint-visitor-keys "^4.2.1"
+
+espree@^9.6.0, espree@^9.6.1:
+ version "9.6.1"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
+ integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
+ dependencies:
+ acorn "^8.9.0"
+ acorn-jsx "^5.3.2"
+ eslint-visitor-keys "^3.4.1"
+
+esquery@^1.4.2:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7"
+ integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==
+ dependencies:
+ estraverse "^5.1.0"
+
+esrecurse@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+ integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+ dependencies:
+ estraverse "^5.2.0"
+
+estraverse@^4.1.1:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+ integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.1.0, estraverse@^5.2.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
+ integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+esutils@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
+ integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+eventemitter3@^4.0.4:
+ version "4.0.7"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+ integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+ integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-diff@^1.1.2:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
+ integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
+
+fast-glob@^3.2.9:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
+ integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
+ dependencies:
+ "@nodelib/fs.stat" "^2.0.2"
+ "@nodelib/fs.walk" "^1.2.3"
+ glob-parent "^5.1.2"
+ merge2 "^1.3.0"
+ micromatch "^4.0.8"
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+ integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fast-levenshtein@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+ integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
+
+fastq@^1.6.0:
+ version "1.19.1"
+ resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5"
+ integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==
+ dependencies:
+ reusify "^1.0.4"
+
+file-entry-cache@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
+ integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
+ dependencies:
+ flat-cache "^3.0.4"
+
+fill-range@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
+ integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+find-up@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+ integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+ dependencies:
+ locate-path "^6.0.0"
+ path-exists "^4.0.0"
+
+flat-cache@^3.0.4:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee"
+ integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==
+ dependencies:
+ flatted "^3.2.9"
+ keyv "^4.5.3"
+ rimraf "^3.0.2"
+
+flatted@^3.2.9:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
+ integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
+
+for-each@^0.3.3, for-each@^0.3.5:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47"
+ integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==
+ dependencies:
+ is-callable "^1.2.7"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+
+function.prototype.name@^1.1.6, function.prototype.name@^1.1.8:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78"
+ integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==
+ dependencies:
+ call-bind "^1.0.8"
+ call-bound "^1.0.3"
+ define-properties "^1.2.1"
+ functions-have-names "^1.2.3"
+ hasown "^2.0.2"
+ is-callable "^1.2.7"
+
+functions-have-names@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
+ integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
+
+get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
+ integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
+ dependencies:
+ call-bind-apply-helpers "^1.0.2"
+ es-define-property "^1.0.1"
+ es-errors "^1.3.0"
+ es-object-atoms "^1.1.1"
+ function-bind "^1.1.2"
+ get-proto "^1.0.1"
+ gopd "^1.2.0"
+ has-symbols "^1.1.0"
+ hasown "^2.0.2"
+ math-intrinsics "^1.1.0"
+
+get-proto@^1.0.0, get-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
+ integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
+ dependencies:
+ dunder-proto "^1.0.1"
+ es-object-atoms "^1.0.0"
+
+get-symbol-description@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee"
+ integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==
+ dependencies:
+ call-bound "^1.0.3"
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.6"
+
+glob-parent@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+glob-parent@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
+ integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
+ dependencies:
+ is-glob "^4.0.3"
+
+glob@^7.1.3:
+ version "7.2.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
+ integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.1.1"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+globals@^13.19.0:
+ version "13.24.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171"
+ integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==
+ dependencies:
+ type-fest "^0.20.2"
+
+globals@^14.0.0:
+ version "14.0.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
+ integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
+
+globalthis@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236"
+ integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==
+ dependencies:
+ define-properties "^1.2.1"
+ gopd "^1.0.1"
+
+globby@^11.1.0:
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
+ integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
+ dependencies:
+ array-union "^2.1.0"
+ dir-glob "^3.0.1"
+ fast-glob "^3.2.9"
+ ignore "^5.2.0"
+ merge2 "^1.4.1"
+ slash "^3.0.0"
+
+gopd@^1.0.1, gopd@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
+ integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
+
+graphemer@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
+ integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
+
+has-bigints@^1.0.2:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe"
+ integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==
+
+has-flag@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+ integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
+ integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
+ dependencies:
+ es-define-property "^1.0.0"
+
+has-proto@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5"
+ integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==
+ dependencies:
+ dunder-proto "^1.0.0"
+
+has-symbols@^1.0.3, has-symbols@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
+ integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
+
+has-tostringtag@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
+ integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
+ dependencies:
+ has-symbols "^1.0.3"
+
+hasown@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
+ integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
+ dependencies:
+ function-bind "^1.1.2"
+
+ignore@^5.2.0:
+ version "5.3.2"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
+ integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
+
+import-fresh@^3.2.1:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf"
+ integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==
+ dependencies:
+ parent-module "^1.0.0"
+ resolve-from "^4.0.0"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+ integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+internal-slot@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961"
+ integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==
+ dependencies:
+ es-errors "^1.3.0"
+ hasown "^2.0.2"
+ side-channel "^1.1.0"
+
+is-array-buffer@^3.0.4, is-array-buffer@^3.0.5:
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280"
+ integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==
+ dependencies:
+ call-bind "^1.0.8"
+ call-bound "^1.0.3"
+ get-intrinsic "^1.2.6"
+
+is-async-function@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523"
+ integrity sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==
+ dependencies:
+ async-function "^1.0.0"
+ call-bound "^1.0.3"
+ get-proto "^1.0.1"
+ has-tostringtag "^1.0.2"
+ safe-regex-test "^1.1.0"
+
+is-bigint@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672"
+ integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==
+ dependencies:
+ has-bigints "^1.0.2"
+
+is-boolean-object@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e"
+ integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==
+ dependencies:
+ call-bound "^1.0.3"
+ has-tostringtag "^1.0.2"
+
+is-callable@^1.2.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
+ integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
+
+is-core-module@^2.13.0, is-core-module@^2.16.0, is-core-module@^2.16.1:
+ version "2.16.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4"
+ integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==
+ dependencies:
+ hasown "^2.0.2"
+
+is-data-view@^1.0.1, is-data-view@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e"
+ integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==
+ dependencies:
+ call-bound "^1.0.2"
+ get-intrinsic "^1.2.6"
+ is-typed-array "^1.1.13"
+
+is-date-object@^1.0.5, is-date-object@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7"
+ integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==
+ dependencies:
+ call-bound "^1.0.2"
+ has-tostringtag "^1.0.2"
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-finalizationregistry@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90"
+ integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==
+ dependencies:
+ call-bound "^1.0.3"
+
+is-generator-function@^1.0.10:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.0.tgz#bf3eeda931201394f57b5dba2800f91a238309ca"
+ integrity sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==
+ dependencies:
+ call-bound "^1.0.3"
+ get-proto "^1.0.0"
+ has-tostringtag "^1.0.2"
+ safe-regex-test "^1.1.0"
+
+is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-map@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e"
+ integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==
+
+is-negative-zero@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747"
+ integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==
+
+is-number-object@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541"
+ integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==
+ dependencies:
+ call-bound "^1.0.3"
+ has-tostringtag "^1.0.2"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-path-inside@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
+ integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
+
+is-regex@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22"
+ integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==
+ dependencies:
+ call-bound "^1.0.2"
+ gopd "^1.2.0"
+ has-tostringtag "^1.0.2"
+ hasown "^2.0.2"
+
+is-set@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d"
+ integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==
+
+is-shared-array-buffer@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f"
+ integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==
+ dependencies:
+ call-bound "^1.0.3"
+
+is-string@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9"
+ integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==
+ dependencies:
+ call-bound "^1.0.3"
+ has-tostringtag "^1.0.2"
+
+is-symbol@^1.0.4, is-symbol@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634"
+ integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==
+ dependencies:
+ call-bound "^1.0.2"
+ has-symbols "^1.1.0"
+ safe-regex-test "^1.1.0"
+
+is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15:
+ version "1.1.15"
+ resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b"
+ integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==
+ dependencies:
+ which-typed-array "^1.1.16"
+
+is-weakmap@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd"
+ integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==
+
+is-weakref@^1.0.2, is-weakref@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293"
+ integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==
+ dependencies:
+ call-bound "^1.0.3"
+
+is-weakset@^2.0.3:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca"
+ integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==
+ dependencies:
+ call-bound "^1.0.3"
+ get-intrinsic "^1.2.6"
+
+isarray@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
+ integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+js-tiktoken@^1.0.12:
+ version "1.0.21"
+ resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.21.tgz#368a9957591a30a62997dd0c4cf30866f00f8221"
+ integrity sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==
+ dependencies:
+ base64-js "^1.5.1"
+
+js-yaml@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+ integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+ dependencies:
+ argparse "^2.0.1"
+
+json-buffer@3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
+ integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-stable-stringify-without-jsonify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+ integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
+
+json5@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
+ integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
+ dependencies:
+ minimist "^1.2.0"
+
+keyv@^4.5.3:
+ version "4.5.4"
+ resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
+ integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
+ dependencies:
+ json-buffer "3.0.1"
+
+langsmith@^0.3.46:
+ version "0.3.66"
+ resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.3.66.tgz#33f13458c6d1086b5f530a25f00c38acd448e0fa"
+ integrity sha512-d50FJ25HPAT2e/6u7oPAYFYH7uvVhxf7vThAOE5tP6YFIUHwLMBmJj8R4Z7APp5jF4/m8upvbK4J4jP5UIN+Eg==
+ dependencies:
+ "@types/uuid" "^10.0.0"
+ chalk "^4.1.2"
+ console-table-printer "^2.12.1"
+ p-queue "^6.6.2"
+ p-retry "4"
+ semver "^7.6.3"
+ uuid "^10.0.0"
+
+levn@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
+ integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
+ dependencies:
+ prelude-ls "^1.2.1"
+ type-check "~0.4.0"
+
+locate-path@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
+ integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+ dependencies:
+ p-locate "^5.0.0"
+
+lodash.merge@^4.6.2:
+ version "4.6.2"
+ resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
+ integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
+
+math-intrinsics@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
+ integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
+
+merge2@^1.3.0, merge2@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+ integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
+ integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
+ dependencies:
+ braces "^3.0.3"
+ picomatch "^2.3.1"
+
+minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@^1.2.0, minimist@^1.2.6:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+ integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
+ms@^2.1.1, ms@^2.1.3:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+mustache@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
+ integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
+
+natural-compare-lite@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4"
+ integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+ integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
+
+object-inspect@^1.13.3, object-inspect@^1.13.4:
+ version "1.13.4"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213"
+ integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==
+
+object-keys@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+ integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object.assign@^4.1.7:
+ version "4.1.7"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d"
+ integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==
+ dependencies:
+ call-bind "^1.0.8"
+ call-bound "^1.0.3"
+ define-properties "^1.2.1"
+ es-object-atoms "^1.0.0"
+ has-symbols "^1.1.0"
+ object-keys "^1.1.1"
+
+object.fromentries@^2.0.8:
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65"
+ integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==
+ dependencies:
+ call-bind "^1.0.7"
+ define-properties "^1.2.1"
+ es-abstract "^1.23.2"
+ es-object-atoms "^1.0.0"
+
+object.groupby@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e"
+ integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==
+ dependencies:
+ call-bind "^1.0.7"
+ define-properties "^1.2.1"
+ es-abstract "^1.23.2"
+
+object.values@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216"
+ integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==
+ dependencies:
+ call-bind "^1.0.8"
+ call-bound "^1.0.3"
+ define-properties "^1.2.1"
+ es-object-atoms "^1.0.0"
+
+once@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+ dependencies:
+ wrappy "1"
+
+optionator@^0.9.3:
+ version "0.9.4"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
+ integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==
+ dependencies:
+ deep-is "^0.1.3"
+ fast-levenshtein "^2.0.6"
+ levn "^0.4.1"
+ prelude-ls "^1.2.1"
+ type-check "^0.4.0"
+ word-wrap "^1.2.5"
+
+own-keys@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358"
+ integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==
+ dependencies:
+ get-intrinsic "^1.2.6"
+ object-keys "^1.1.1"
+ safe-push-apply "^1.0.0"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+ integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
+
+p-limit@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+ integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+ dependencies:
+ yocto-queue "^0.1.0"
+
+p-locate@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
+ integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+ dependencies:
+ p-limit "^3.0.2"
+
+p-queue@^6.6.2:
+ version "6.6.2"
+ resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426"
+ integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==
+ dependencies:
+ eventemitter3 "^4.0.4"
+ p-timeout "^3.2.0"
+
+p-retry@4:
+ version "4.6.2"
+ resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16"
+ integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==
+ dependencies:
+ "@types/retry" "0.12.0"
+ retry "^0.13.1"
+
+p-timeout@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
+ integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
+ dependencies:
+ p-finally "^1.0.0"
+
+parent-module@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+ integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+ dependencies:
+ callsites "^3.0.0"
+
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+ integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+
+path-key@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+ integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+path-type@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+ integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+possible-typed-array-names@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae"
+ integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==
+
+prelude-ls@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
+ integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
+
+prettier-linter-helpers@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
+ integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
+ dependencies:
+ fast-diff "^1.1.2"
+
+prettier@^3.3.3:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393"
+ integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==
+
+punycode@^2.1.0:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
+ integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
+
+queue-microtask@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+ integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9"
+ integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==
+ dependencies:
+ call-bind "^1.0.8"
+ define-properties "^1.2.1"
+ es-abstract "^1.23.9"
+ es-errors "^1.3.0"
+ es-object-atoms "^1.0.0"
+ get-intrinsic "^1.2.7"
+ get-proto "^1.0.1"
+ which-builtin-type "^1.2.1"
+
+regexp.prototype.flags@^1.5.4:
+ version "1.5.4"
+ resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19"
+ integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==
+ dependencies:
+ call-bind "^1.0.8"
+ define-properties "^1.2.1"
+ es-errors "^1.3.0"
+ get-proto "^1.0.1"
+ gopd "^1.2.0"
+ set-function-name "^2.0.2"
+
+resolve-from@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+ integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve@^1.22.4:
+ version "1.22.10"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39"
+ integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==
+ dependencies:
+ is-core-module "^2.16.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
+retry@^0.13.1:
+ version "0.13.1"
+ resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
+ integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==
+
+reusify@^1.0.4:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f"
+ integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
+
+rimraf@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+ integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+ dependencies:
+ glob "^7.1.3"
+
+run-parallel@^1.1.9:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+ integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+ dependencies:
+ queue-microtask "^1.2.2"
+
+safe-array-concat@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3"
+ integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==
+ dependencies:
+ call-bind "^1.0.8"
+ call-bound "^1.0.2"
+ get-intrinsic "^1.2.6"
+ has-symbols "^1.1.0"
+ isarray "^2.0.5"
+
+safe-push-apply@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5"
+ integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==
+ dependencies:
+ es-errors "^1.3.0"
+ isarray "^2.0.5"
+
+safe-regex-test@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1"
+ integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==
+ dependencies:
+ call-bound "^1.0.2"
+ es-errors "^1.3.0"
+ is-regex "^1.2.1"
+
+semver@^6.3.1:
+ version "6.3.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
+ integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
+
+semver@^7.3.7, semver@^7.6.3:
+ version "7.7.2"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
+ integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
+
+set-function-length@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
+ integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
+ dependencies:
+ define-data-property "^1.1.4"
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+ get-intrinsic "^1.2.4"
+ gopd "^1.0.1"
+ has-property-descriptors "^1.0.2"
+
+set-function-name@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985"
+ integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==
+ dependencies:
+ define-data-property "^1.1.4"
+ es-errors "^1.3.0"
+ functions-have-names "^1.2.3"
+ has-property-descriptors "^1.0.2"
+
+set-proto@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e"
+ integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==
+ dependencies:
+ dunder-proto "^1.0.1"
+ es-errors "^1.3.0"
+ es-object-atoms "^1.0.0"
+
+shebang-command@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+ integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+ dependencies:
+ shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+ integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+side-channel-list@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
+ integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==
+ dependencies:
+ es-errors "^1.3.0"
+ object-inspect "^1.13.3"
+
+side-channel-map@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42"
+ integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==
+ dependencies:
+ call-bound "^1.0.2"
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.5"
+ object-inspect "^1.13.3"
+
+side-channel-weakmap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea"
+ integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==
+ dependencies:
+ call-bound "^1.0.2"
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.5"
+ object-inspect "^1.13.3"
+ side-channel-map "^1.0.1"
+
+side-channel@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
+ integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
+ dependencies:
+ es-errors "^1.3.0"
+ object-inspect "^1.13.3"
+ side-channel-list "^1.0.0"
+ side-channel-map "^1.0.1"
+ side-channel-weakmap "^1.0.2"
+
+simple-wcswidth@^1.0.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz#66722f37629d5203f9b47c5477b1225b85d6525b"
+ integrity sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==
+
+slash@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+ integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+
+stop-iteration-iterator@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad"
+ integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==
+ dependencies:
+ es-errors "^1.3.0"
+ internal-slot "^1.1.0"
+
+string.prototype.trim@^1.2.10:
+ version "1.2.10"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81"
+ integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==
+ dependencies:
+ call-bind "^1.0.8"
+ call-bound "^1.0.2"
+ define-data-property "^1.1.4"
+ define-properties "^1.2.1"
+ es-abstract "^1.23.5"
+ es-object-atoms "^1.0.0"
+ has-property-descriptors "^1.0.2"
+
+string.prototype.trimend@^1.0.9:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942"
+ integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==
+ dependencies:
+ call-bind "^1.0.8"
+ call-bound "^1.0.2"
+ define-properties "^1.2.1"
+ es-object-atoms "^1.0.0"
+
+string.prototype.trimstart@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde"
+ integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==
+ dependencies:
+ call-bind "^1.0.7"
+ define-properties "^1.2.1"
+ es-object-atoms "^1.0.0"
+
+strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+ integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
+
+strip-json-comments@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+ integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
+supports-color@^7.1.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+ integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+ dependencies:
+ has-flag "^4.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+text-table@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+ integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+tsconfig-paths@^3.15.0:
+ version "3.15.0"
+ resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4"
+ integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==
+ dependencies:
+ "@types/json5" "^0.0.29"
+ json5 "^1.0.2"
+ minimist "^1.2.6"
+ strip-bom "^3.0.0"
+
+tslib@^1.8.1:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+ integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
+tsutils@^3.21.0:
+ version "3.21.0"
+ resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
+ integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
+ dependencies:
+ tslib "^1.8.1"
+
+turbo-darwin-64@2.5.6:
+ version "2.5.6"
+ resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.5.6.tgz#d694492bdd16359b31918f9d33234bccc44f853e"
+ integrity sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A==
+
+turbo-darwin-arm64@2.5.6:
+ version "2.5.6"
+ resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.5.6.tgz#b8800a1613bc06ded2445b0337ed146c1dbac234"
+ integrity sha512-LyiG+rD7JhMfYwLqB6k3LZQtYn8CQQUePbpA8mF/hMLPAekXdJo1g0bUPw8RZLwQXUIU/3BU7tXENvhSGz5DPA==
+
+turbo-linux-64@2.5.6:
+ version "2.5.6"
+ resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.5.6.tgz#23938cee385e358a1363316b6d7464899354349d"
+ integrity sha512-GOcUTT0xiT/pSnHL4YD6Yr3HreUhU8pUcGqcI2ksIF9b2/r/kRHwGFcsHgpG3+vtZF/kwsP0MV8FTlTObxsYIA==
+
+turbo-linux-arm64@2.5.6:
+ version "2.5.6"
+ resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.5.6.tgz#1e36c543497ffbefed7e479bd9dbe594de457321"
+ integrity sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ==
+
+turbo-windows-64@2.5.6:
+ version "2.5.6"
+ resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.5.6.tgz#abbb9a8226d0b2b293fc7dbea0e0869a95323545"
+ integrity sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg==
+
+turbo-windows-arm64@2.5.6:
+ version "2.5.6"
+ resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.5.6.tgz#81af8538b338ef3b3814db0ed1d9ea31d30ff72d"
+ integrity sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q==
+
+turbo@^2.5.0:
+ version "2.5.6"
+ resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.5.6.tgz#610810b87037520cc2c0f94decba552d3a6e770b"
+ integrity sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w==
+ optionalDependencies:
+ turbo-darwin-64 "2.5.6"
+ turbo-darwin-arm64 "2.5.6"
+ turbo-linux-64 "2.5.6"
+ turbo-linux-arm64 "2.5.6"
+ turbo-windows-64 "2.5.6"
+ turbo-windows-arm64 "2.5.6"
+
+type-check@^0.4.0, type-check@~0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
+ integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
+ dependencies:
+ prelude-ls "^1.2.1"
+
+type-fest@^0.20.2:
+ version "0.20.2"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
+ integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
+
+typed-array-buffer@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536"
+ integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==
+ dependencies:
+ call-bound "^1.0.3"
+ es-errors "^1.3.0"
+ is-typed-array "^1.1.14"
+
+typed-array-byte-length@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce"
+ integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==
+ dependencies:
+ call-bind "^1.0.8"
+ for-each "^0.3.3"
+ gopd "^1.2.0"
+ has-proto "^1.2.0"
+ is-typed-array "^1.1.14"
+
+typed-array-byte-offset@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355"
+ integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==
+ dependencies:
+ available-typed-arrays "^1.0.7"
+ call-bind "^1.0.8"
+ for-each "^0.3.3"
+ gopd "^1.2.0"
+ has-proto "^1.2.0"
+ is-typed-array "^1.1.15"
+ reflect.getprototypeof "^1.0.9"
+
+typed-array-length@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d"
+ integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==
+ dependencies:
+ call-bind "^1.0.7"
+ for-each "^0.3.3"
+ gopd "^1.0.1"
+ is-typed-array "^1.1.13"
+ possible-typed-array-names "^1.0.0"
+ reflect.getprototypeof "^1.0.6"
+
+typescript@^5.3.3:
+ version "5.9.2"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6"
+ integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==
+
+unbox-primitive@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2"
+ integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==
+ dependencies:
+ call-bound "^1.0.3"
+ has-bigints "^1.0.2"
+ has-symbols "^1.1.0"
+ which-boxed-primitive "^1.1.1"
+
+uri-js@^4.2.2:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+ integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+ dependencies:
+ punycode "^2.1.0"
+
+uuid@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
+ integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
+
+uuid@^9.0.0:
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
+ integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
+
+which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e"
+ integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==
+ dependencies:
+ is-bigint "^1.1.0"
+ is-boolean-object "^1.2.1"
+ is-number-object "^1.1.1"
+ is-string "^1.1.1"
+ is-symbol "^1.1.1"
+
+which-builtin-type@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e"
+ integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==
+ dependencies:
+ call-bound "^1.0.2"
+ function.prototype.name "^1.1.6"
+ has-tostringtag "^1.0.2"
+ is-async-function "^2.0.0"
+ is-date-object "^1.1.0"
+ is-finalizationregistry "^1.1.0"
+ is-generator-function "^1.0.10"
+ is-regex "^1.2.1"
+ is-weakref "^1.0.2"
+ isarray "^2.0.5"
+ which-boxed-primitive "^1.1.0"
+ which-collection "^1.0.2"
+ which-typed-array "^1.1.16"
+
+which-collection@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0"
+ integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==
+ dependencies:
+ is-map "^2.0.3"
+ is-set "^2.0.3"
+ is-weakmap "^2.0.2"
+ is-weakset "^2.0.3"
+
+which-typed-array@^1.1.16, which-typed-array@^1.1.19:
+ version "1.1.19"
+ resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956"
+ integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==
+ dependencies:
+ available-typed-arrays "^1.0.7"
+ call-bind "^1.0.8"
+ call-bound "^1.0.4"
+ for-each "^0.3.5"
+ get-proto "^1.0.1"
+ gopd "^1.2.0"
+ has-tostringtag "^1.0.2"
+
+which@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ dependencies:
+ isexe "^2.0.0"
+
+word-wrap@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
+ integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+yocto-queue@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+ integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+
+zod-to-json-schema@^3.22.3:
+ version "3.24.6"
+ resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz#5920f020c4d2647edfbb954fa036082b92c9e12d"
+ integrity sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==
+
+zod@^3.23.8, zod@^3.25.32:
+ version "3.25.76"
+ resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34"
+ integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
diff --git a/langgraph/source/libs/cli/langgraph_cli/__init__.py b/langgraph/source/libs/cli/langgraph_cli/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b084a6099bc5219d8f022f20c898de6b83f4df9
--- /dev/null
+++ b/langgraph/source/libs/cli/langgraph_cli/__init__.py
@@ -0,0 +1 @@
+__version__ = "0.4.12"
diff --git a/langgraph/source/libs/cli/langgraph_cli/__main__.py b/langgraph/source/libs/cli/langgraph_cli/__main__.py
new file mode 100644
index 0000000000000000000000000000000000000000..98dcca0c2836ae53d67260be8a5431b855e461bb
--- /dev/null
+++ b/langgraph/source/libs/cli/langgraph_cli/__main__.py
@@ -0,0 +1,4 @@
+from .cli import cli
+
+if __name__ == "__main__":
+ cli()
diff --git a/langgraph/source/libs/cli/langgraph_cli/analytics.py b/langgraph/source/libs/cli/langgraph_cli/analytics.py
new file mode 100644
index 0000000000000000000000000000000000000000..0495d268a42289ceb396c5e3db1bb62f4a74adc6
--- /dev/null
+++ b/langgraph/source/libs/cli/langgraph_cli/analytics.py
@@ -0,0 +1,98 @@
+import functools
+import json
+import os
+import pathlib
+import platform
+import threading
+import urllib.error
+import urllib.request
+from typing import Any, TypedDict
+
+from langgraph_cli.constants import (
+ DEFAULT_CONFIG,
+ DEFAULT_PORT,
+ SUPABASE_PUBLIC_API_KEY,
+ SUPABASE_URL,
+)
+from langgraph_cli.version import __version__
+
+
+class LogData(TypedDict):
+ os: str
+ os_version: str
+ python_version: str
+ cli_version: str
+ cli_command: str
+ params: dict[str, Any]
+
+
+def get_anonymized_params(kwargs: dict[str, Any]) -> dict[str, bool]:
+ params = {}
+
+ # anonymize params with values
+ if config := kwargs.get("config"):
+ if config != pathlib.Path(DEFAULT_CONFIG).resolve():
+ params["config"] = True
+
+ if port := kwargs.get("port"):
+ if port != DEFAULT_PORT:
+ params["port"] = True
+
+ if kwargs.get("docker_compose"):
+ params["docker_compose"] = True
+
+ if kwargs.get("debugger_port"):
+ params["debugger_port"] = True
+
+ if kwargs.get("postgres_uri"):
+ params["postgres_uri"] = True
+
+ # pick up exact values for boolean flags
+ for boolean_param in ["recreate", "pull", "watch", "wait", "verbose"]:
+ if kwargs.get(boolean_param):
+ params[boolean_param] = kwargs[boolean_param]
+
+ return params
+
+
+def log_data(data: LogData) -> None:
+ headers = {
+ "Content-Type": "application/json",
+ "apikey": SUPABASE_PUBLIC_API_KEY,
+ "User-Agent": "Mozilla/5.0",
+ }
+ supabase_url = SUPABASE_URL
+
+ req = urllib.request.Request(
+ f"{supabase_url}/rest/v1/logs",
+ data=json.dumps(data).encode("utf-8"),
+ headers=headers,
+ method="POST",
+ )
+
+ try:
+ urllib.request.urlopen(req)
+ except urllib.error.URLError:
+ pass
+
+
+def log_command(func):
+ @functools.wraps(func)
+ def decorator(*args, **kwargs):
+ if os.getenv("LANGGRAPH_CLI_NO_ANALYTICS") == "1":
+ return func(*args, **kwargs)
+
+ data = {
+ "os": platform.system(),
+ "os_version": platform.version(),
+ "python_version": platform.python_version(),
+ "cli_version": __version__,
+ "cli_command": func.__name__,
+ "params": get_anonymized_params(kwargs),
+ }
+
+ background_thread = threading.Thread(target=log_data, args=(data,))
+ background_thread.start()
+ return func(*args, **kwargs)
+
+ return decorator
diff --git a/langgraph/source/libs/cli/langgraph_cli/cli.py b/langgraph/source/libs/cli/langgraph_cli/cli.py
new file mode 100644
index 0000000000000000000000000000000000000000..af78f90009c7e0458c5a39bc6b29c8412b51f59d
--- /dev/null
+++ b/langgraph/source/libs/cli/langgraph_cli/cli.py
@@ -0,0 +1,878 @@
+"""CLI entrypoint for LangGraph API server."""
+
+import os
+import pathlib
+import shutil
+import sys
+from collections.abc import Callable, Sequence
+
+import click
+import click.exceptions
+from click import secho
+
+import langgraph_cli.config
+import langgraph_cli.docker
+from langgraph_cli.analytics import log_command
+from langgraph_cli.config import Config
+from langgraph_cli.constants import DEFAULT_CONFIG, DEFAULT_PORT
+from langgraph_cli.docker import DockerCapabilities
+from langgraph_cli.exec import Runner, subp_exec
+from langgraph_cli.progress import Progress
+from langgraph_cli.templates import TEMPLATE_HELP_STRING, create_new
+from langgraph_cli.util import warn_non_wolfi_distro
+from langgraph_cli.version import __version__
+
+OPT_DOCKER_COMPOSE = click.option(
+ "--docker-compose",
+ "-d",
+ help="Advanced: Path to docker-compose.yml file with additional services to launch.",
+ type=click.Path(
+ exists=True,
+ file_okay=True,
+ dir_okay=False,
+ resolve_path=True,
+ path_type=pathlib.Path,
+ ),
+)
+OPT_CONFIG = click.option(
+ "--config",
+ "-c",
+ help="""Path to configuration file declaring dependencies, graphs and environment variables.
+
+ \b
+ Config file must be a JSON file that has the following keys:
+ - "dependencies": array of dependencies for langgraph API server. Dependencies can be one of the following:
+ - ".", which would look for local python packages, as well as pyproject.toml, setup.py or requirements.txt in the app directory
+ - "./local_package"
+ - "
+ - "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, 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
+
+ \b
+ Example:
+ langgraph up -c langgraph.json
+
+ \b
+ Example:
+ {
+ "dependencies": [
+ "langchain_openai",
+ "./your_package"
+ ],
+ "graphs": {
+ "my_graph_id": "./your_package/your_file.py:variable"
+ },
+ "env": "./.env"
+ }
+
+ \b
+ Example:
+ {
+ "python_version": "3.11",
+ "dependencies": [
+ "langchain_openai",
+ "."
+ ],
+ "graphs": {
+ "my_graph_id": "./your_package/your_file.py:variable"
+ },
+ "env": {
+ "OPENAI_API_KEY": "secret-key"
+ }
+ }
+
+ Defaults to looking for langgraph.json in the current directory.""",
+ default=DEFAULT_CONFIG,
+ type=click.Path(
+ exists=True,
+ file_okay=True,
+ dir_okay=False,
+ resolve_path=True,
+ path_type=pathlib.Path,
+ ),
+)
+OPT_PORT = click.option(
+ "--port",
+ "-p",
+ type=int,
+ default=DEFAULT_PORT,
+ show_default=True,
+ help="""
+ Port to expose.
+
+ \b
+ Example:
+ langgraph up --port 8000
+ \b
+ """,
+)
+OPT_RECREATE = click.option(
+ "--recreate/--no-recreate",
+ default=False,
+ show_default=True,
+ help="Recreate containers even if their configuration and image haven't changed",
+)
+OPT_PULL = click.option(
+ "--pull/--no-pull",
+ default=True,
+ show_default=True,
+ help="""
+ Pull latest images. Use --no-pull for running the server with locally-built images.
+
+ \b
+ Example:
+ langgraph up --no-pull
+ \b
+ """,
+)
+OPT_VERBOSE = click.option(
+ "--verbose",
+ is_flag=True,
+ default=False,
+ help="Show more output from the server logs",
+)
+OPT_WATCH = click.option("--watch", is_flag=True, help="Restart on file changes")
+OPT_DEBUGGER_PORT = click.option(
+ "--debugger-port",
+ type=int,
+ help="Pull the debugger image locally and serve the UI on specified port",
+)
+OPT_DEBUGGER_BASE_URL = click.option(
+ "--debugger-base-url",
+ type=str,
+ help="URL used by the debugger to access LangGraph API. Defaults to http://127.0.0.1:[PORT]",
+)
+
+OPT_POSTGRES_URI = click.option(
+ "--postgres-uri",
+ help="Postgres URI to use for the database. Defaults to launching a local database",
+)
+
+OPT_API_VERSION = click.option(
+ "--api-version",
+ type=str,
+ help="API server version to use for the base image. If unspecified, the latest version will be used.",
+)
+
+
+@click.group()
+@click.version_option(version=__version__, prog_name="LangGraph CLI")
+def cli():
+ pass
+
+
+@OPT_RECREATE
+@OPT_PULL
+@OPT_PORT
+@OPT_DOCKER_COMPOSE
+@OPT_CONFIG
+@OPT_VERBOSE
+@OPT_DEBUGGER_PORT
+@OPT_DEBUGGER_BASE_URL
+@OPT_WATCH
+@OPT_POSTGRES_URI
+@OPT_API_VERSION
+@click.option(
+ "--image",
+ type=str,
+ default=None,
+ help="Docker image to use for the langgraph-api service. If specified, skips building and uses this image directly."
+ " Useful if you want to test against an image already built using `langgraph build`.",
+)
+@click.option(
+ "--base-image",
+ default=None,
+ help="Base image to use for the LangGraph API server. Pin to specific versions using version tags. Defaults to langchain/langgraph-api or langchain/langgraphjs-api."
+ "\n\n \b\nExamples:\n --base-image langchain/langgraph-server:0.2.18 # Pin to a specific patch version"
+ "\n --base-image langchain/langgraph-server:0.2 # Pin to a minor version (Python)",
+)
+@click.option(
+ "--wait",
+ is_flag=True,
+ help="Wait for services to start before returning. Implies --detach",
+)
+@cli.command(help="🚀 Launch LangGraph API server.")
+@log_command
+def up(
+ config: pathlib.Path,
+ docker_compose: pathlib.Path | None,
+ port: int,
+ recreate: bool,
+ pull: bool,
+ watch: bool,
+ wait: bool,
+ verbose: bool,
+ debugger_port: int | None,
+ debugger_base_url: str | None,
+ postgres_uri: str | None,
+ api_version: str | None,
+ image: str | None,
+ base_image: str | None,
+):
+ click.secho("Starting LangGraph API server...", fg="green")
+ click.secho(
+ """For local dev, requires env var LANGSMITH_API_KEY with access to LangSmith Deployment.
+For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KEY.""",
+ )
+ with Runner() as runner, Progress(message="Pulling...") as set:
+ capabilities = langgraph_cli.docker.check_capabilities(runner)
+ args, stdin = prepare(
+ runner,
+ capabilities=capabilities,
+ config_path=config,
+ docker_compose=docker_compose,
+ port=port,
+ pull=pull,
+ watch=watch,
+ verbose=verbose,
+ debugger_port=debugger_port,
+ debugger_base_url=debugger_base_url,
+ postgres_uri=postgres_uri,
+ api_version=api_version,
+ image=image,
+ base_image=base_image,
+ )
+ # add up + options
+ args.extend(["up", "--remove-orphans"])
+ if recreate:
+ args.extend(["--force-recreate", "--renew-anon-volumes"])
+ try:
+ runner.run(subp_exec("docker", "volume", "rm", "langgraph-data"))
+ except click.exceptions.Exit:
+ pass
+ if watch:
+ args.append("--watch")
+ if wait:
+ args.append("--wait")
+ else:
+ args.append("--abort-on-container-exit")
+ # run docker compose
+ set("Building...")
+
+ def on_stdout(line: str):
+ if "unpacking to docker.io" in line:
+ set("Starting...")
+ elif "Application startup complete" in line:
+ debugger_origin = (
+ f"http://localhost:{debugger_port}"
+ if debugger_port
+ else "https://smith.langchain.com"
+ )
+ debugger_base_url_query = (
+ debugger_base_url or f"http://127.0.0.1:{port}"
+ )
+ set("")
+ sys.stdout.write(
+ f"""Ready!
+- API: http://localhost:{port}
+- Docs: http://localhost:{port}/docs
+- LangGraph Studio: {debugger_origin}/studio/?baseUrl={debugger_base_url_query}
+"""
+ )
+ sys.stdout.flush()
+ return True
+
+ if capabilities.compose_type == "plugin":
+ compose_cmd = ["docker", "compose"]
+ elif capabilities.compose_type == "standalone":
+ compose_cmd = ["docker-compose"]
+
+ runner.run(
+ subp_exec(
+ *compose_cmd,
+ *args,
+ input=stdin,
+ verbose=verbose,
+ on_stdout=on_stdout,
+ )
+ )
+
+
+def _build(
+ runner,
+ set: Callable[[str], None],
+ config: pathlib.Path,
+ config_json: dict,
+ base_image: str | None,
+ api_version: str | None,
+ pull: bool,
+ tag: str,
+ passthrough: Sequence[str] = (),
+ install_command: str | None = None,
+ build_command: str | None = None,
+):
+ # pull latest images
+ if pull:
+ runner.run(
+ subp_exec(
+ "docker",
+ "pull",
+ langgraph_cli.config.docker_tag(config_json, base_image, api_version),
+ verbose=True,
+ )
+ )
+ set("Building...")
+ # apply options
+ args = [
+ "-f",
+ "-", # stdin
+ "-t",
+ tag,
+ ]
+ # determine build context: use current directory for JS projects, config parent for Python
+ is_js_project = config_json.get("node_version") and not config_json.get(
+ "python_version"
+ )
+ # build/install commands only apply to JS projects for now
+ # without install/build command, JS projects will follow the old behavior
+ if is_js_project and (build_command or install_command):
+ build_context = str(pathlib.Path.cwd())
+ else:
+ build_context = str(config.parent)
+
+ # apply config
+ stdin, additional_contexts = langgraph_cli.config.config_to_docker(
+ config_path=config,
+ config=config_json,
+ base_image=base_image,
+ api_version=api_version,
+ install_command=install_command,
+ build_command=build_command,
+ build_context=build_context,
+ )
+ # add additional_contexts
+ if additional_contexts:
+ for k, v in additional_contexts.items():
+ args.extend(["--build-context", f"{k}={v}"])
+ runner.run(
+ subp_exec(
+ "docker",
+ "build",
+ *args,
+ *passthrough,
+ build_context,
+ input=stdin,
+ verbose=True,
+ )
+ )
+
+
+@OPT_CONFIG
+@OPT_PULL
+@click.option(
+ "--tag",
+ "-t",
+ help="""Tag for the docker image.
+
+ \b
+ Example:
+ langgraph build -t my-image
+
+ \b
+ """,
+ required=True,
+)
+@click.option(
+ "--base-image",
+ help="Base image to use for the LangGraph API server. Pin to specific versions using version tags. Defaults to langchain/langgraph-api or langchain/langgraphjs-api."
+ "\n\n \b\nExamples:\n --base-image langchain/langgraph-server:0.2.18 # Pin to a specific patch version"
+ "\n --base-image langchain/langgraph-server:0.2 # Pin to a minor version (Python)",
+)
+@OPT_API_VERSION
+@click.option(
+ "--install-command",
+ help="Custom install command to run from the build context root. If not provided, auto-detects based on package manager files.",
+)
+@click.option(
+ "--build-command",
+ help="Custom build command to run from the langgraph.json directory. If not provided, uses default build process.",
+)
+@click.argument("docker_build_args", nargs=-1, type=click.UNPROCESSED)
+@cli.command(
+ help="📦 Build LangGraph API server Docker image.",
+ context_settings=dict(
+ ignore_unknown_options=True,
+ ),
+)
+@log_command
+def build(
+ config: pathlib.Path,
+ docker_build_args: Sequence[str],
+ base_image: str | None,
+ api_version: str | None,
+ pull: bool,
+ tag: str,
+ install_command: str | None,
+ build_command: str | None,
+):
+ with Runner() as runner, Progress(message="Pulling...") as set:
+ if shutil.which("docker") is None:
+ raise click.UsageError("Docker not installed") from None
+ config_json = langgraph_cli.config.validate_config_file(config)
+ warn_non_wolfi_distro(config_json)
+ _build(
+ runner,
+ set,
+ config,
+ config_json,
+ base_image,
+ api_version,
+ pull,
+ tag,
+ docker_build_args,
+ install_command,
+ build_command,
+ )
+
+
+def _get_docker_ignore_content() -> str:
+ """Return the content of a .dockerignore file.
+
+ This file is used to exclude files and directories from the Docker build context.
+
+ It may be overly broad, but it's better to be safe than sorry.
+
+ The main goal is to exclude .env files by default.
+ """
+ return """\
+# Ignore node_modules and other dependency directories
+node_modules
+bower_components
+vendor
+
+# Ignore logs and temporary files
+*.log
+*.tmp
+*.swp
+
+# Ignore .env files and other environment files
+.env
+.env.*
+*.local
+
+# Ignore git-related files
+.git
+.gitignore
+
+# Ignore Docker-related files and configs
+.dockerignore
+docker-compose.yml
+
+# Ignore build and cache directories
+dist
+build
+.cache
+__pycache__
+
+# Ignore IDE and editor configurations
+.vscode
+.idea
+*.sublime-project
+*.sublime-workspace
+.DS_Store # macOS-specific
+
+# Ignore test and coverage files
+coverage
+*.coverage
+*.test.js
+*.spec.js
+tests
+"""
+
+
+@OPT_CONFIG
+@click.argument("save_path", type=click.Path(resolve_path=True))
+@cli.command(
+ help="🐳 Generate a Dockerfile for the LangGraph API server, with Docker Compose options."
+)
+@click.option(
+ # Add a flag for adding a docker-compose.yml file as part of the output
+ "--add-docker-compose",
+ help=(
+ "Add additional files for running the LangGraph API server with "
+ "docker-compose. These files include a docker-compose.yml, .env file, "
+ "and a .dockerignore file."
+ ),
+ is_flag=True,
+)
+@click.option(
+ "--base-image",
+ help="Base image to use for the LangGraph API server. Pin to specific versions using version tags. Defaults to langchain/langgraph-api or langchain/langgraphjs-api."
+ "\n\n \b\nExamples:\n --base-image langchain/langgraph-server:0.2.18 # Pin to a specific patch version"
+ "\n --base-image langchain/langgraph-server:0.2 # Pin to a minor version (Python)",
+)
+@OPT_API_VERSION
+@log_command
+def dockerfile(
+ save_path: str,
+ config: pathlib.Path,
+ add_docker_compose: bool,
+ base_image: str | None = None,
+ api_version: str | None = None,
+) -> None:
+ save_path = pathlib.Path(save_path).absolute()
+ secho(f"🔍 Validating configuration at path: {config}", fg="yellow")
+ config_json = langgraph_cli.config.validate_config_file(config)
+ warn_non_wolfi_distro(config_json)
+ secho("✅ Configuration validated!", fg="green")
+
+ secho(f"📝 Generating Dockerfile at {save_path}", fg="yellow")
+ dockerfile, additional_contexts = langgraph_cli.config.config_to_docker(
+ config_path=config,
+ config=config_json,
+ base_image=base_image,
+ api_version=api_version,
+ )
+ with open(str(save_path), "w", encoding="utf-8") as f:
+ f.write(dockerfile)
+ secho("✅ Created: Dockerfile", fg="green")
+
+ if additional_contexts:
+ additional_contexts_str = ",".join(
+ f"{k}={v}" for k, v in additional_contexts.items()
+ )
+ secho(
+ f"""📝 Run docker build with these additional build contexts `--build-context {additional_contexts_str}`""",
+ fg="yellow",
+ )
+
+ if add_docker_compose:
+ # Add docker compose and related files
+ # Add .dockerignore file in the same directory as the Dockerfile
+ with open(str(save_path.parent / ".dockerignore"), "w", encoding="utf-8") as f:
+ f.write(_get_docker_ignore_content())
+ secho("✅ Created: .dockerignore", fg="green")
+
+ # Generate a docker-compose.yml file
+ path = str(save_path.parent / "docker-compose.yml")
+ with open(path, "w", encoding="utf-8") as f:
+ with Runner() as runner:
+ capabilities = langgraph_cli.docker.check_capabilities(runner)
+
+ compose_dict = langgraph_cli.docker.compose_as_dict(
+ capabilities,
+ port=8123,
+ base_image=base_image,
+ )
+ # Add .env file to the docker-compose.yml for the langgraph-api service
+ compose_dict["services"]["langgraph-api"]["env_file"] = [".env"]
+ # Add the Dockerfile to the build context
+ compose_dict["services"]["langgraph-api"]["build"] = {
+ "context": ".",
+ "dockerfile": save_path.name,
+ }
+ # Add the base_image as build arg if provided
+ if base_image:
+ compose_dict["services"]["langgraph-api"]["build"]["args"] = {
+ "BASE_IMAGE": base_image
+ }
+ f.write(langgraph_cli.docker.dict_to_yaml(compose_dict))
+ secho("✅ Created: docker-compose.yml", fg="green")
+
+ # Check if the .env file exists in the same directory as the Dockerfile
+ if not (save_path.parent / ".env").exists():
+ # Also add an empty .env file
+ with open(str(save_path.parent / ".env"), "w", encoding="utf-8") as f:
+ f.writelines(
+ [
+ "# Uncomment the following line to add your LangSmith API key",
+ "\n",
+ "# LANGSMITH_API_KEY=your-api-key",
+ "\n",
+ "# Or if you have a LangSmith Deployment license key, "
+ "then uncomment the following line: ",
+ "\n",
+ "# LANGGRAPH_CLOUD_LICENSE_KEY=your-license-key",
+ "\n",
+ "# Add any other environment variables go below...",
+ ]
+ )
+
+ secho("✅ Created: .env", fg="green")
+ else:
+ # Do nothing since the .env file already exists. Not a great
+ # idea to overwrite in case the user has added custom env vars set
+ # in the .env file already.
+ secho("➖ Skipped: .env. It already exists!", fg="yellow")
+
+ secho(
+ f"🎉 Files generated successfully at path {save_path.parent}!",
+ fg="cyan",
+ bold=True,
+ )
+
+
+@click.option(
+ "--host",
+ default="127.0.0.1",
+ help="Network interface to bind the development server to. Default 127.0.0.1 is recommended for security. Only use 0.0.0.0 in trusted networks",
+)
+@click.option(
+ "--port",
+ default=2024,
+ type=int,
+ help="Port number to bind the development server to. Example: langgraph dev --port 8000",
+)
+@click.option(
+ "--no-reload",
+ is_flag=True,
+ help="Disable automatic reloading when code changes are detected",
+)
+@click.option(
+ "--config",
+ type=click.Path(exists=True),
+ default="langgraph.json",
+ help="Path to configuration file declaring dependencies, graphs and environment variables",
+)
+@click.option(
+ "--n-jobs-per-worker",
+ default=None,
+ type=int,
+ help="Maximum number of concurrent jobs each worker process can handle. Default: 10",
+)
+@click.option(
+ "--no-browser",
+ is_flag=True,
+ help="Skip automatically opening the browser when the server starts",
+)
+@click.option(
+ "--debug-port",
+ default=None,
+ 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,
+)
+@click.option(
+ "--studio-url",
+ type=str,
+ default=None,
+ help="URL of the LangGraph Studio instance to connect to. Defaults to https://smith.langchain.com",
+)
+@click.option(
+ "--allow-blocking",
+ is_flag=True,
+ help="Don't raise errors for synchronous I/O blocking operations in your code.",
+ default=False,
+)
+@click.option(
+ "--tunnel",
+ is_flag=True,
+ help="Expose the local server via a public tunnel (in this case, Cloudflare) "
+ "for remote frontend access. This avoids issues with browsers "
+ "or networks blocking localhost connections.",
+ default=False,
+)
+@click.option(
+ "--server-log-level",
+ type=str,
+ default="WARNING",
+ help="Set the log level for the API server.",
+)
+@cli.command(
+ "dev",
+ help="🏃♀️➡️ Run LangGraph API server in development mode with hot reloading and debugging support",
+)
+@log_command
+def dev(
+ host: str,
+ port: int,
+ no_reload: bool,
+ config: str,
+ n_jobs_per_worker: int | None,
+ no_browser: bool,
+ debug_port: int | None,
+ wait_for_client: bool,
+ studio_url: str | None,
+ allow_blocking: bool,
+ tunnel: bool,
+ server_log_level: str,
+):
+ """CLI entrypoint for running the LangGraph API server."""
+ try:
+ from langgraph_api.cli import run_server # type: ignore
+ except ImportError:
+ py_version_msg = ""
+ if sys.version_info < (3, 11):
+ py_version_msg = (
+ "\n\nNote: The in-mem server requires Python 3.11 or higher to be installed."
+ f" You are currently using Python {sys.version_info.major}.{sys.version_info.minor}."
+ ' Please upgrade your Python version before installing "langgraph-cli[inmem]".'
+ )
+ try:
+ from importlib import util
+
+ if not util.find_spec("langgraph_api"):
+ raise click.UsageError(
+ "Required package 'langgraph-api' is not installed.\n"
+ "Please install it with:\n\n"
+ ' pip install -U "langgraph-cli[inmem]"'
+ f"{py_version_msg}"
+ ) from None
+ except ImportError:
+ raise click.UsageError(
+ "Could not verify package installation. Please ensure Python is up to date and\n"
+ "langgraph-cli is installed with the 'inmem' extra: pip install -U \"langgraph-cli[inmem]\""
+ f"{py_version_msg}"
+ ) from None
+ raise click.UsageError(
+ "Could not import run_server. This likely means your installation is incomplete.\n"
+ "Please ensure langgraph-cli is installed with the 'inmem' extra: pip install -U \"langgraph-cli[inmem]\""
+ f"{py_version_msg}"
+ ) from None
+
+ config_json = langgraph_cli.config.validate_config_file(pathlib.Path(config))
+ if config_json.get("node_version"):
+ raise click.UsageError(
+ "In-mem server for JS graphs is not supported in this version of the LangGraph CLI. Please use `npx @langchain/langgraph-cli` instead."
+ ) from None
+
+ 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(
+ host,
+ port,
+ not no_reload,
+ graphs,
+ n_jobs_per_worker=n_jobs_per_worker,
+ open_browser=not no_browser,
+ debug_port=debug_port,
+ env=config_json.get("env"),
+ store=config_json.get("store"),
+ wait_for_client=wait_for_client,
+ auth=config_json.get("auth"),
+ http=config_json.get("http"),
+ ui=config_json.get("ui"),
+ ui_config=config_json.get("ui_config"),
+ webhooks=config_json.get("webhooks"),
+ studio_url=studio_url,
+ allow_blocking=allow_blocking,
+ tunnel=tunnel,
+ server_level=server_log_level,
+ )
+
+
+@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: str | None, template: str | None) -> None:
+ """Create a new LangGraph project from a template."""
+ return create_new(path, template)
+
+
+def prepare_args_and_stdin(
+ *,
+ capabilities: DockerCapabilities,
+ config_path: pathlib.Path,
+ config: Config,
+ docker_compose: pathlib.Path | None,
+ port: int,
+ watch: bool,
+ debugger_port: int | None = None,
+ debugger_base_url: str | None = None,
+ postgres_uri: str | None = None,
+ api_version: str | None = None,
+ # Like "my-tag" (if you already built it locally)
+ image: str | None = None,
+ # Like "langchain/langgraphjs-api" or "langchain/langgraph-api
+ base_image: str | None = None,
+) -> tuple[list[str], str]:
+ assert config_path.exists(), f"Config file not found: {config_path}"
+ # prepare args
+ stdin = langgraph_cli.docker.compose(
+ capabilities,
+ port=port,
+ debugger_port=debugger_port,
+ debugger_base_url=debugger_base_url,
+ postgres_uri=postgres_uri,
+ image=image, # Pass image to compose YAML generator
+ base_image=base_image,
+ api_version=api_version,
+ )
+ args = [
+ "--project-directory",
+ str(config_path.parent),
+ ]
+ # apply options
+ if docker_compose:
+ args.extend(["-f", str(docker_compose)])
+ args.extend(["-f", "-"]) # stdin
+ # apply config
+ stdin += langgraph_cli.config.config_to_compose(
+ config_path,
+ config,
+ watch=watch,
+ base_image=langgraph_cli.config.default_base_image(config),
+ api_version=api_version,
+ image=image,
+ )
+ return args, stdin
+
+
+def prepare(
+ runner,
+ *,
+ capabilities: DockerCapabilities,
+ config_path: pathlib.Path,
+ docker_compose: pathlib.Path | None,
+ port: int,
+ pull: bool,
+ watch: bool,
+ verbose: bool,
+ debugger_port: int | None = None,
+ debugger_base_url: str | None = None,
+ postgres_uri: str | None = None,
+ api_version: str | None = None,
+ image: str | None = None,
+ base_image: str | None = None,
+) -> tuple[list[str], str]:
+ """Prepare the arguments and stdin for running the LangGraph API server."""
+ config_json = langgraph_cli.config.validate_config_file(config_path)
+ warn_non_wolfi_distro(config_json)
+ # pull latest images
+ if pull:
+ runner.run(
+ subp_exec(
+ "docker",
+ "pull",
+ langgraph_cli.config.docker_tag(config_json, base_image, api_version),
+ verbose=verbose,
+ )
+ )
+
+ args, stdin = prepare_args_and_stdin(
+ capabilities=capabilities,
+ config_path=config_path,
+ config=config_json,
+ docker_compose=docker_compose,
+ port=port,
+ watch=watch,
+ debugger_port=debugger_port,
+ debugger_base_url=debugger_base_url or f"http://127.0.0.1:{port}",
+ postgres_uri=postgres_uri,
+ api_version=api_version,
+ image=image,
+ base_image=base_image,
+ )
+ return args, stdin
diff --git a/langgraph/source/libs/cli/langgraph_cli/config.py b/langgraph/source/libs/cli/langgraph_cli/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..393f54f6db494abd371bf4207e2d3fbbae7a4972
--- /dev/null
+++ b/langgraph/source/libs/cli/langgraph_cli/config.py
@@ -0,0 +1,1304 @@
+import json
+import os
+import pathlib
+import re
+import textwrap
+from collections import Counter
+from typing import Literal, NamedTuple
+
+import click
+
+from langgraph_cli.schemas import Config, Distros
+
+MIN_NODE_VERSION = "20"
+DEFAULT_NODE_VERSION = "20"
+
+MIN_PYTHON_VERSION = "3.11"
+DEFAULT_PYTHON_VERSION = "3.11"
+
+DEFAULT_IMAGE_DISTRO = "debian"
+
+
+_BUILD_TOOLS = ("pip", "setuptools", "wheel")
+
+
+def _get_pip_cleanup_lines(
+ install_cmd: str,
+ to_uninstall: tuple[str] | None,
+ pip_installer: Literal["uv", "pip"],
+) -> str:
+ commands = [
+ f"""# -- Ensure user deps didn't inadvertently overwrite langgraph-api
+RUN mkdir -p /api/langgraph_api /api/langgraph_runtime /api/langgraph_license && \
+touch /api/langgraph_api/__init__.py /api/langgraph_runtime/__init__.py /api/langgraph_license/__init__.py
+RUN PYTHONDONTWRITEBYTECODE=1 {install_cmd} --no-cache-dir --no-deps -e /api
+# -- End of ensuring user deps didn't inadvertently overwrite langgraph-api --
+# -- Removing build deps from the final image ~<:===~~~ --"""
+ ]
+ if to_uninstall:
+ for pack in to_uninstall:
+ if pack not in _BUILD_TOOLS:
+ raise ValueError(
+ f"Invalid build tool: {pack}; must be one of {', '.join(_BUILD_TOOLS)}"
+ )
+ packs_str = " ".join(sorted(to_uninstall))
+ commands.append(f"RUN pip uninstall -y {packs_str}")
+ # Ensure the directories are removed entirely
+ packages_rm = " ".join(
+ f"/usr/local/lib/python*/site-packages/{pack}*" for pack in to_uninstall
+ )
+ if "pip" in to_uninstall:
+ packages_rm += ' && find /usr/local/bin -name "pip*" -delete || true'
+ commands.append(f"RUN rm -rf {packages_rm}")
+ wolfi_packages_rm = " ".join(
+ f"/usr/lib/python*/site-packages/{pack}*" for pack in to_uninstall
+ )
+ if "pip" in to_uninstall:
+ wolfi_packages_rm += ' && find /usr/bin -name "pip*" -delete || true'
+ commands.append(f"RUN rm -rf {wolfi_packages_rm}")
+ if pip_installer == "uv":
+ commands.append(
+ f"RUN uv pip uninstall --system {packs_str} && rm /usr/bin/uv /usr/bin/uvx"
+ )
+ else:
+ if pip_installer == "uv":
+ commands.append(
+ "RUN rm /usr/bin/uv /usr/bin/uvx\n# -- End of build deps removal --"
+ )
+ return "\n".join(commands)
+
+
+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("-")[0].split("."))
+ return (major, minor)
+ except ValueError:
+ 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 _is_node_graph(spec: str | dict) -> bool:
+ """Check if a graph is a Node.js graph based on the file extension."""
+ if isinstance(spec, dict):
+ spec = spec.get("path")
+
+ file_path = spec.split(":")[0]
+ file_ext = os.path.splitext(file_path)[1]
+
+ return file_ext in [
+ ".ts",
+ ".mts",
+ ".cts",
+ ".js",
+ ".mjs",
+ ".cjs",
+ ]
+
+
+def validate_config(config: Config) -> Config:
+ """Validate a configuration dictionary."""
+
+ graphs = config.get("graphs", {})
+
+ some_node = any(_is_node_graph(spec) for spec in graphs.values())
+ some_python = any(not _is_node_graph(spec) for spec in graphs.values())
+
+ node_version = config.get(
+ "node_version", DEFAULT_NODE_VERSION if some_node else None
+ )
+ python_version = config.get(
+ "python_version", DEFAULT_PYTHON_VERSION if some_python else None
+ )
+
+ image_distro = config.get("image_distro", DEFAULT_IMAGE_DISTRO)
+ internal_docker_tag = config.get("_INTERNAL_docker_tag")
+ api_version = config.get("api_version")
+ if internal_docker_tag:
+ if api_version:
+ raise click.UsageError(
+ "Cannot specify both _INTERNAL_docker_tag and api_version."
+ )
+ if api_version:
+ try:
+ parts = tuple(map(int, api_version.split("-")[0].split(".")))
+ if len(parts) > 3:
+ raise ValueError(
+ "Version must be major or major.minor or major.minor.patch."
+ )
+ except TypeError:
+ raise click.UsageError(f"Invalid version format: {api_version}") from None
+
+ config = {
+ "node_version": node_version,
+ "python_version": python_version,
+ "pip_config_file": config.get("pip_config_file"),
+ "pip_installer": config.get("pip_installer", "auto"),
+ "base_image": config.get("base_image"),
+ "image_distro": image_distro,
+ "dependencies": config.get("dependencies", []),
+ "dockerfile_lines": config.get("dockerfile_lines", []),
+ "graphs": config.get("graphs", {}),
+ "env": config.get("env", {}),
+ "store": config.get("store"),
+ "auth": config.get("auth"),
+ "encryption": config.get("encryption"),
+ "http": config.get("http"),
+ # Pass through webhooks config so it can be injected into the image
+ "webhooks": config.get("webhooks"),
+ "checkpointer": config.get("checkpointer"),
+ "ui": config.get("ui"),
+ "ui_config": config.get("ui_config"),
+ "keep_pkg_tools": config.get("keep_pkg_tools"),
+ }
+ if internal_docker_tag:
+ config["_INTERNAL_docker_tag"] = internal_docker_tag
+ if api_version:
+ config["api_version"] = api_version
+
+ if config.get("node_version"):
+ 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"]
+ if not pyversion.count(".") == 1 or not all(
+ part.isdigit() for part in pyversion.split("-")[0].split(".")
+ ):
+ raise click.UsageError(
+ 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. "
+ "Add at least one dependency to 'dependencies' list."
+ )
+
+ if not config.get("graphs"):
+ raise click.UsageError(
+ "No graphs found in config. Add at least one graph to 'graphs' dictionary."
+ )
+
+ # Validate image_distro config
+ if image_distro := config.get("image_distro"):
+ if image_distro not in Distros.__args__:
+ raise click.UsageError(
+ f"Invalid image_distro: '{image_distro}'. "
+ "Must be one of 'debian', 'bullseye', or 'bookworm'."
+ )
+
+ if pip_installer := config.get("pip_installer"):
+ if pip_installer not in ["auto", "pip", "uv"]:
+ raise click.UsageError(
+ f"Invalid pip_installer: '{pip_installer}'. "
+ "Must be 'auto', 'pip', or 'uv'."
+ )
+
+ # Validate auth config
+ if auth_conf := config.get("auth"):
+ if "path" in auth_conf:
+ if ":" not in auth_conf["path"]:
+ raise ValueError(
+ f"Invalid auth.path format: '{auth_conf['path']}'. "
+ "Must be in format './path/to/file.py:attribute_name'"
+ )
+ # Validate encryption config
+ if encryption_conf := config.get("encryption"):
+ if "path" in encryption_conf:
+ if ":" not in encryption_conf["path"]:
+ raise ValueError(
+ f"Invalid encryption.path format: '{encryption_conf['path']}'. "
+ "Must be in format './path/to/file.py:attribute_name'"
+ )
+ if http_conf := config.get("http"):
+ if "app" in http_conf:
+ if ":" not in http_conf["app"]:
+ raise ValueError(
+ f"Invalid http.app format: '{http_conf['app']}'. "
+ "Must be in format './path/to/file.py:attribute_name'"
+ )
+ if keep_pkg_tools := config.get("keep_pkg_tools"):
+ if isinstance(keep_pkg_tools, list):
+ for tool in keep_pkg_tools:
+ if tool not in _BUILD_TOOLS:
+ raise ValueError(
+ f"Invalid keep_pkg_tools: '{tool}'. "
+ "Must be one of 'pip', 'setuptools', 'wheel'."
+ )
+ elif keep_pkg_tools is True:
+ pass
+ else:
+ raise ValueError(
+ f"Invalid keep_pkg_tools: '{keep_pkg_tools}'. "
+ "Must be bool or list[str] (with values"
+ " 'pip', 'setuptools', and/or 'wheel')."
+ )
+ return config
+
+
+def validate_config_file(config_path: pathlib.Path) -> Config:
+ """Load and validate a configuration file."""
+ 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):
+ """A container for referencing and managing local Python dependencies.
+
+ A "local dependency" is any entry in the config's `dependencies` list
+ that starts with "." (dot), denoting a relative path
+ to a local directory containing Python code.
+
+ For each local dependency, the system inspects its directory to
+ determine how it should be installed inside the Docker container.
+
+ Specifically, we detect:
+
+ - **Real packages**: Directories containing a `pyproject.toml` or a `setup.py`.
+ These can be installed with pip as a regular Python package.
+ - **Faux packages**: Directories that do not include a `pyproject.toml` or
+ `setup.py` but do contain Python files and possibly an `__init__.py`. For
+ these, the code dynamically generates a minimal `pyproject.toml` in the
+ Docker image so that they can still be installed with pip.
+ - **Requirements files**: If a local dependency directory
+ has a `requirements.txt`, it is tracked so that those dependencies
+ can be installed within the Docker container before installing the local package.
+
+ Attributes:
+ pip_reqs: A list of (host_requirements_path, container_requirements_path)
+ tuples. Each entry points to a local `requirements.txt` file and where
+ it should be placed inside the Docker container before running `pip install`.
+
+ real_pkgs: A dictionary mapping a local directory path (host side) to a
+ tuple of (dependency_string, container_package_path). These directories
+ contain the necessary files (e.g., `pyproject.toml` or `setup.py`) to be
+ installed as a standard Python package with pip.
+
+ faux_pkgs: A dictionary mapping a local directory path (host side) to a
+ tuple of (dependency_string, container_package_path). For these
+ directories—called "faux packages"—the code will generate a minimal
+ `pyproject.toml` inside the Docker image. This ensures that pip
+ recognizes them as installable packages, even though they do not
+ natively include packaging metadata.
+
+ working_dir: The path inside the Docker container to use as the working
+ directory. If the local dependency `"."` is present in the config, this
+ field captures the path where that dependency will appear in the
+ container (e.g., `/deps/` or similar). Otherwise, it may be `None`.
+
+ additional_contexts: A list of paths to directories that contain local
+ dependencies in parent directories. These directories are added to the
+ Docker build context to ensure that the Dockerfile can access them.
+ """
+
+ pip_reqs: list[tuple[pathlib.Path, str]]
+ real_pkgs: dict[pathlib.Path, tuple[str, str]]
+ faux_pkgs: dict[pathlib.Path, tuple[str, str]]
+ # if . is in dependencies, use it as working_dir
+ working_dir: str | None = None
+ # if there are local dependencies in parent directories, use additional_contexts
+ additional_contexts: list[pathlib.Path] = None
+
+
+def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps:
+ config_path = config_path.resolve()
+ # ensure reserved package names are not used
+ reserved = {
+ "src",
+ "langgraph-api",
+ "langgraph_api",
+ "langgraph",
+ "langchain-core",
+ "langchain_core",
+ "pydantic",
+ "orjson",
+ "fastapi",
+ "uvicorn",
+ "psycopg",
+ "httpx",
+ "langsmith",
+ }
+ counter = Counter()
+
+ def check_reserved(name: str, ref: str):
+ if name in reserved:
+ raise ValueError(
+ f"Package name '{name}' used in local dep '{ref}' is reserved. "
+ "Rename the directory."
+ )
+ reserved.add(name)
+
+ pip_reqs = []
+ real_pkgs = {}
+ faux_pkgs = {}
+ working_dir: str | None = None
+ additional_contexts: list[pathlib.Path] = []
+
+ for local_dep in config["dependencies"]:
+ if not local_dep.startswith("."):
+ # If the dependency is not a local path, skip it
+ continue
+
+ # Verify that the local dependency can be resolved
+ # (e.g., this would raise an informative error if a user mistyped a path).
+ resolved = (config_path.parent / local_dep).resolve()
+
+ # validate local dependency
+ if not resolved.exists():
+ raise FileNotFoundError(f"Could not find local dependency: {resolved}")
+ elif not resolved.is_dir():
+ raise NotADirectoryError(
+ f"Local dependency must be a directory: {resolved}"
+ )
+ elif resolved == config_path.parent:
+ pass
+ elif config_path.parent not in resolved.parents:
+ additional_contexts.append(resolved)
+
+ # Check for pyproject.toml or setup.py
+ # If found, treat as a real package, if not treat as a faux package.
+ # For faux packages, we'll also check for presence of requirements.txt.
+ files = os.listdir(resolved)
+ if "pyproject.toml" in files or "setup.py" in files:
+ # real package
+
+ # assign a unique folder name
+ container_name = resolved.name
+ if counter[container_name] > 0:
+ container_name += f"_{counter[container_name]}"
+ counter[container_name] += 1
+ # add to deps
+ real_pkgs[resolved] = (local_dep, container_name)
+ # set working_dir
+ if local_dep == ".":
+ working_dir = f"/deps/{container_name}"
+ else:
+ # We could not find a pyproject.toml or setup.py, so treat as a faux package
+ if any(file == "__init__.py" for file in files):
+ # flat layout
+ if "-" in resolved.name:
+ raise ValueError(
+ f"Package name '{resolved.name}' contains a hyphen. "
+ "Rename the directory to use it as flat-layout package."
+ )
+ check_reserved(resolved.name, local_dep)
+ container_path = f"/deps/outer-{resolved.name}/{resolved.name}"
+ else:
+ # src layout
+ container_path = f"/deps/outer-{resolved.name}/src"
+ for file in files:
+ rfile = resolved / file
+ if (
+ rfile.is_dir()
+ and file != "__pycache__"
+ and not file.startswith(".")
+ ):
+ try:
+ for subfile in os.listdir(rfile):
+ if subfile.endswith(".py"):
+ check_reserved(file, local_dep)
+ break
+ except PermissionError:
+ pass
+ faux_pkgs[resolved] = (local_dep, container_path)
+ if local_dep == ".":
+ working_dir = container_path
+
+ # If the faux package has a requirements.txt, we'll add
+ # the path to the list of requirements to install.
+ if "requirements.txt" in files:
+ rfile = resolved / "requirements.txt"
+ pip_reqs.append(
+ (
+ rfile,
+ f"{container_path}/requirements.txt",
+ )
+ )
+
+ return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir, additional_contexts)
+
+
+def _update_graph_paths(
+ config_path: pathlib.Path, config: Config, local_deps: LocalDeps
+) -> None:
+ """Remap each graph's import path to the correct in-container path.
+
+ The config may contain entries in `graphs` that look like this:
+ {
+ "my_graph": "./mygraphs/main.py:graph_function"
+ }
+ or
+ {
+ "my_graph": "./src/some_subdir/my_file.py:my_graph"
+ }
+ which indicate a local file (on the host) followed by a colon and a
+ callable/object attribute within that file.
+
+ During the Docker build, local directories are copied into special
+ `/deps/` subdirectories, so they can be installed or referenced in
+ the container. This function updates each graph's import path to
+ reflect its new location **inside** the Docker container.
+
+ Paths inside the container must be POSIX-style paths (even if
+ the host system is Windows).
+
+ Args:
+ config_path: The path to the config file (e.g. `langgraph.json`).
+ config: The validated configuration dictionary.
+ local_deps: An object containing references to local dependencies:
+ - real Python packages (with a `pyproject.toml` or `setup.py`)
+ - “faux” packages that need minimal metadata to be installable
+ - potential `requirements.txt` for local dependencies
+ - container work directory (if "." is in `dependencies`)
+
+ Raises:
+ ValueError: If the import string is not in the format `:`
+ or if the referenced local file is not found in `dependencies`.
+ FileNotFoundError: If the local file (module) does not actually exist on disk.
+ IsADirectoryError: If `module_str` points to a directory instead of a file.
+ """
+ for graph_id, data in config["graphs"].items():
+ if isinstance(data, dict):
+ # Then we're looking for a 'path' key
+ if "path" not in data:
+ raise ValueError(
+ f"Graph '{graph_id}' must contain a 'path' key if "
+ f" it is a dictionary."
+ )
+ import_str = data["path"]
+ elif isinstance(data, str):
+ import_str = data
+ else:
+ raise ValueError(
+ f"Graph '{graph_id}' must be a string or a dictionary with a 'path' key."
+ )
+
+ module_str, _, attr_str = import_str.partition(":")
+ if not module_str or not attr_str:
+ message = (
+ 'Import string "{import_str}" must be in format ":".'
+ )
+ raise ValueError(message.format(import_str=import_str))
+
+ # Check for either forward slash or backslash in the module string
+ # to determine if it's a file path.
+ if "/" in module_str or "\\" in module_str:
+ # Resolve the local path properly on the current OS
+ resolved = (config_path.parent / module_str).resolve()
+ if not resolved.exists():
+ raise FileNotFoundError(f"Could not find local module: {resolved}")
+ elif not resolved.is_file():
+ raise IsADirectoryError(f"Local module must be a file: {resolved}")
+ else:
+ for path in local_deps.real_pkgs:
+ if resolved.is_relative_to(path):
+ container_path = (
+ pathlib.Path("/deps")
+ / path.name
+ / resolved.relative_to(path)
+ )
+ module_str = container_path.as_posix()
+ break
+ else:
+ for faux_pkg, (_, destpath) in local_deps.faux_pkgs.items():
+ if resolved.is_relative_to(faux_pkg):
+ container_subpath = resolved.relative_to(faux_pkg)
+ # Construct the final path, ensuring POSIX style
+ module_str = f"{destpath}/{container_subpath.as_posix()}"
+ break
+ else:
+ raise ValueError(
+ f"Module '{import_str}' not found in 'dependencies' list. "
+ "Add its containing package to 'dependencies' list."
+ )
+ # update the config
+ if isinstance(data, dict):
+ config["graphs"][graph_id]["path"] = f"{module_str}:{attr_str}"
+ else:
+ config["graphs"][graph_id] = f"{module_str}:{attr_str}"
+
+
+def _update_auth_path(
+ config_path: pathlib.Path, config: Config, local_deps: LocalDeps
+) -> None:
+ """Update auth.path to use Docker container paths."""
+ auth_conf = config.get("auth")
+ if not auth_conf or not (path_str := auth_conf.get("path")):
+ return
+
+ module_str, sep, attr_str = path_str.partition(":")
+ if not sep or not module_str.startswith("."):
+ return # Already validated or absolute path
+
+ resolved = config_path.parent / module_str
+ if not resolved.exists():
+ raise FileNotFoundError(f"Auth file not found: {resolved} (from {path_str})")
+ if not resolved.is_file():
+ raise IsADirectoryError(f"Auth path must be a file: {resolved}")
+
+ # Check faux packages first (higher priority)
+ for faux_path, (_, destpath) in local_deps.faux_pkgs.items():
+ if resolved.is_relative_to(faux_path):
+ new_path = f"{destpath}/{resolved.relative_to(faux_path)}:{attr_str}"
+ auth_conf["path"] = new_path
+ return
+
+ # Check real packages
+ for real_path in local_deps.real_pkgs:
+ if resolved.is_relative_to(real_path):
+ new_path = (
+ f"/deps/{real_path.name}/{resolved.relative_to(real_path)}:{attr_str}"
+ )
+ auth_conf["path"] = new_path
+ return
+
+ raise ValueError(
+ f"Auth file '{resolved}' not covered by dependencies.\n"
+ "Add its parent directory to the 'dependencies' array in your config.\n"
+ f"Current dependencies: {config['dependencies']}"
+ )
+
+
+def _update_encryption_path(
+ config_path: pathlib.Path, config: Config, local_deps: LocalDeps
+) -> None:
+ """Update encryption.path to use Docker container paths."""
+ encryption_conf = config.get("encryption")
+ if not encryption_conf or not (path_str := encryption_conf.get("path")):
+ return
+
+ module_str, sep, attr_str = path_str.partition(":")
+ if not sep or not module_str.startswith("."):
+ return # Already validated or absolute path
+
+ resolved = config_path.parent / module_str
+ if not resolved.exists():
+ raise FileNotFoundError(
+ f"Encryption file not found: {resolved} (from {path_str})"
+ )
+ if not resolved.is_file():
+ raise IsADirectoryError(f"Encryption path must be a file: {resolved}")
+
+ # Check faux packages first (higher priority)
+ for faux_path, (_, destpath) in local_deps.faux_pkgs.items():
+ if resolved.is_relative_to(faux_path):
+ new_path = f"{destpath}/{resolved.relative_to(faux_path)}:{attr_str}"
+ encryption_conf["path"] = new_path
+ return
+
+ # Check real packages
+ for real_path in local_deps.real_pkgs:
+ if resolved.is_relative_to(real_path):
+ new_path = (
+ f"/deps/{real_path.name}/{resolved.relative_to(real_path)}:{attr_str}"
+ )
+ encryption_conf["path"] = new_path
+ return
+
+ raise ValueError(
+ f"Encryption file '{resolved}' not covered by dependencies.\n"
+ "Add its parent directory to the 'dependencies' array in your config.\n"
+ f"Current dependencies: {config['dependencies']}"
+ )
+
+
+def _update_http_app_path(
+ config_path: pathlib.Path, config: Config, local_deps: LocalDeps
+) -> None:
+ """Update the HTTP app path to point to the correct location in the Docker container.
+
+ Similar to _update_graph_paths, this ensures that if a custom app is specified via
+ a local file path, that file is included in the Docker build context and its path
+ is updated to point to the correct location in the container.
+ """
+ if not (http_config := config.get("http")) or not (
+ app_str := http_config.get("app")
+ ):
+ return
+
+ module_str, _, attr_str = app_str.partition(":")
+ if not module_str or not attr_str:
+ message = (
+ 'Import string "{import_str}" must be in format ":".'
+ )
+ raise ValueError(message.format(import_str=app_str))
+
+ # Check if it's a file path
+ if "/" in module_str or "\\" in module_str:
+ # Resolve the local path properly on the current OS
+ resolved = (config_path.parent / module_str).resolve()
+ if not resolved.exists():
+ raise FileNotFoundError(f"Could not find HTTP app module: {resolved}")
+ elif not resolved.is_file():
+ raise IsADirectoryError(f"HTTP app module must be a file: {resolved}")
+ else:
+ for path in local_deps.real_pkgs:
+ if resolved.is_relative_to(path):
+ container_path = (
+ pathlib.Path("/deps") / path.name / resolved.relative_to(path)
+ )
+ module_str = container_path.as_posix()
+ break
+ else:
+ for faux_pkg, (_, destpath) in local_deps.faux_pkgs.items():
+ if resolved.is_relative_to(faux_pkg):
+ container_subpath = resolved.relative_to(faux_pkg)
+ # Construct the final path, ensuring POSIX style
+ module_str = f"{destpath}/{container_subpath.as_posix()}"
+ break
+ else:
+ raise ValueError(
+ f"HTTP app module '{app_str}' not found in 'dependencies' list. "
+ "Add its containing package to 'dependencies' list."
+ )
+ # update the config
+ http_config["app"] = f"{module_str}:{attr_str}"
+
+
+def _get_node_pm_install_cmd(config_path: pathlib.Path, config: Config) -> str:
+ def test_file(file_name):
+ full_path = config_path.parent / file_name
+ try:
+ return full_path.is_file()
+ except OSError:
+ return False
+
+ # inspired by `package-manager-detector`
+ def get_pkg_manager_name():
+ try:
+ with open(config_path.parent / "package.json") as f:
+ pkg = json.load(f)
+
+ if (pkg_manager_name := pkg.get("packageManager")) and isinstance(
+ pkg_manager_name, str
+ ):
+ return pkg_manager_name.lstrip("^").split("@")[0]
+
+ if (
+ dev_engine_name := (
+ (pkg.get("devEngines") or {}).get("packageManager") or {}
+ ).get("name")
+ ) and isinstance(dev_engine_name, str):
+ return dev_engine_name
+
+ return None
+ except Exception:
+ return None
+
+ npm, yarn, pnpm, bun = [
+ test_file("package-lock.json"),
+ test_file("yarn.lock"),
+ test_file("pnpm-lock.yaml"),
+ test_file("bun.lockb"),
+ ]
+
+ if yarn:
+ install_cmd = "yarn install --frozen-lockfile"
+ elif pnpm:
+ install_cmd = "pnpm i --frozen-lockfile"
+ elif npm:
+ install_cmd = "npm ci"
+ elif bun:
+ install_cmd = "bun i"
+ else:
+ pkg_manager_name = get_pkg_manager_name()
+
+ if pkg_manager_name == "yarn":
+ install_cmd = "yarn install"
+ elif pkg_manager_name == "pnpm":
+ install_cmd = "pnpm i"
+ elif pkg_manager_name == "bun":
+ install_cmd = "bun i"
+ else:
+ install_cmd = "npm i"
+
+ return install_cmd
+
+
+semver_pattern = re.compile(r":(\d+(?:\.\d+)?(?:\.\d+)?)(?:-|$)")
+
+
+def _image_supports_uv(base_image: str) -> bool:
+ if base_image == "langchain/langgraph-trial":
+ return False
+ match = semver_pattern.search(base_image)
+ if not match:
+ # Default image (langchain/langgraph-api) supports it.
+ return True
+
+ version_str = match.group(1)
+ version = tuple(map(int, version_str.split(".")))
+ min_uv = (0, 2, 47)
+ return version >= min_uv
+
+
+def get_build_tools_to_uninstall(config: Config) -> tuple[str]:
+ keep_pkg_tools = config.get("keep_pkg_tools")
+ if not keep_pkg_tools:
+ return _BUILD_TOOLS
+ if keep_pkg_tools is True:
+ return ()
+ expected = _BUILD_TOOLS
+ if isinstance(keep_pkg_tools, list):
+ for tool in keep_pkg_tools:
+ if tool not in expected:
+ raise ValueError(
+ f"Invalid build tool to uninstall: {tool}. Expected one of {expected}"
+ )
+ return tuple(sorted(set(_BUILD_TOOLS) - set(keep_pkg_tools)))
+ else:
+ raise ValueError(
+ f"Invalid value for keep_pkg_tools: {keep_pkg_tools}."
+ " Expected True or a list containing any of {expected}."
+ )
+
+
+def python_config_to_docker(
+ config_path: pathlib.Path,
+ config: Config,
+ base_image: str,
+ api_version: str | None = None,
+ *,
+ escape_variables: bool = False,
+) -> tuple[str, dict[str, str]]:
+ """Generate a Dockerfile from the configuration."""
+ pip_installer = config.get("pip_installer", "auto")
+ build_tools_to_uninstall = get_build_tools_to_uninstall(config)
+ if pip_installer == "auto":
+ if _image_supports_uv(base_image):
+ pip_installer = "uv"
+ else:
+ pip_installer = "pip"
+ if pip_installer == "uv":
+ install_cmd = "uv pip install --system"
+ elif pip_installer == "pip":
+ install_cmd = "pip install"
+ else:
+ raise ValueError(f"Invalid pip_installer: {pip_installer}")
+
+ # configure pip
+ local_reqs_pip_install = f"PYTHONDONTWRITEBYTECODE=1 {install_cmd} --no-cache-dir -c /api/constraints.txt"
+ global_reqs_pip_install = f"PYTHONDONTWRITEBYTECODE=1 {install_cmd} --no-cache-dir -c /api/constraints.txt"
+ if config.get("pip_config_file"):
+ local_reqs_pip_install = (
+ f"PIP_CONFIG_FILE=/pipconfig.txt {local_reqs_pip_install}"
+ )
+ global_reqs_pip_install = (
+ f"PIP_CONFIG_FILE=/pipconfig.txt {global_reqs_pip_install}"
+ )
+ pip_config_file_str = (
+ f"ADD {config['pip_config_file']} /pipconfig.txt"
+ if config.get("pip_config_file")
+ else ""
+ )
+
+ # collect dependencies
+ pypi_deps = [dep for dep in config["dependencies"] if not dep.startswith(".")]
+ local_deps = _assemble_local_deps(config_path, config)
+ # Rewrite graph paths, so they point to the correct location in the Docker container
+ _update_graph_paths(config_path, config, local_deps)
+ # Rewrite auth path, so it points to the correct location in the Docker container
+ _update_auth_path(config_path, config, local_deps)
+ # Rewrite encryption path, so it points to the correct location in the Docker container
+ _update_encryption_path(config_path, config, local_deps)
+ # Rewrite HTTP app path, so it points to the correct location in the Docker container
+ _update_http_app_path(config_path, config, local_deps)
+
+ pip_pkgs_str = (
+ f"RUN {local_reqs_pip_install} {' '.join(pypi_deps)}" if pypi_deps else ""
+ )
+ if local_deps.pip_reqs:
+ pip_reqs_str = os.linesep.join(
+ (
+ f"COPY --from=outer-{reqpath.name} requirements.txt {destpath}"
+ if reqpath.parent in local_deps.additional_contexts
+ else f"ADD {reqpath.relative_to(config_path.parent)} {destpath}"
+ )
+ for reqpath, destpath in local_deps.pip_reqs
+ )
+ pip_reqs_str += f"{os.linesep}RUN {local_reqs_pip_install} {' '.join('-r ' + r for _, r in local_deps.pip_reqs)}"
+ pip_reqs_str = f"""# -- Installing local requirements --
+{pip_reqs_str}
+# -- End of local requirements install --"""
+
+ else:
+ pip_reqs_str = ""
+
+ # https://setuptools.pypa.io/en/latest/userguide/datafiles.html#package-data
+ # https://til.simonwillison.net/python/pyproject
+ faux_pkgs_str = f"{os.linesep}{os.linesep}".join(
+ (
+ f"""# -- Adding non-package dependency {fullpath.name} --
+COPY --from=outer-{fullpath.name} . {destpath}"""
+ if fullpath in local_deps.additional_contexts
+ else f"""# -- Adding non-package dependency {fullpath.name} --
+ADD {relpath} {destpath}"""
+ )
+ + f"""
+RUN set -ex && \\
+ for line in '[project]' \\
+ 'name = "{fullpath.name}"' \\
+ 'version = "0.1"' \\
+ '[tool.setuptools.package-data]' \\
+ '"*" = ["**/*"]' \\
+ '[build-system]' \\
+ 'requires = ["setuptools>=61"]' \\
+ 'build-backend = "setuptools.build_meta"'; do \\
+ echo "$line" >> /deps/outer-{fullpath.name}/pyproject.toml; \\
+ done
+# -- End of non-package dependency {fullpath.name} --"""
+ for fullpath, (relpath, destpath) in local_deps.faux_pkgs.items()
+ )
+
+ local_pkgs_str = os.linesep.join(
+ (
+ f"""# -- Adding local package {relpath} --
+COPY --from={name} . /deps/{name}
+# -- End of local package {relpath} --"""
+ if fullpath in local_deps.additional_contexts
+ else f"""# -- Adding local package {relpath} --
+ADD {relpath} /deps/{name}
+# -- End of local package {relpath} --"""
+ )
+ for fullpath, (relpath, name) in local_deps.real_pkgs.items()
+ )
+
+ install_node_str: str = (
+ "RUN /storage/install-node.sh"
+ if (config.get("ui") or config.get("node_version")) and local_deps.working_dir
+ else ""
+ )
+
+ installs = f"{os.linesep}{os.linesep}".join(
+ filter(
+ None,
+ [
+ install_node_str,
+ pip_config_file_str,
+ pip_pkgs_str,
+ pip_reqs_str,
+ local_pkgs_str,
+ faux_pkgs_str,
+ ],
+ )
+ )
+
+ env_vars = []
+
+ if (store_config := config.get("store")) is not None:
+ env_vars.append(f"ENV LANGGRAPH_STORE='{json.dumps(store_config)}'")
+
+ if (auth_config := config.get("auth")) is not None:
+ env_vars.append(f"ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'")
+
+ if (encryption_config := config.get("encryption")) is not None:
+ env_vars.append(f"ENV LANGGRAPH_ENCRYPTION='{json.dumps(encryption_config)}'")
+
+ if (http_config := config.get("http")) is not None:
+ env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
+
+ # Inject webhooks configuration if provided
+ if (webhooks_config := config.get("webhooks")) is not None:
+ env_vars.append(f"ENV LANGGRAPH_WEBHOOKS='{json.dumps(webhooks_config)}'")
+
+ if (checkpointer_config := config.get("checkpointer")) is not None:
+ env_vars.append(
+ f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'"
+ )
+
+ if (ui := config.get("ui")) is not None:
+ env_vars.append(f"ENV LANGGRAPH_UI='{json.dumps(ui)}'")
+
+ if (ui_config := config.get("ui_config")) is not None:
+ env_vars.append(f"ENV LANGGRAPH_UI_CONFIG='{json.dumps(ui_config)}'")
+
+ env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(config['graphs'])}'")
+
+ js_inst_str: str = ""
+ if (config.get("ui") or config.get("node_version")) and local_deps.working_dir:
+ js_inst_str = os.linesep.join(
+ [
+ "# -- Installing JS dependencies --",
+ f"ENV NODE_VERSION={config.get('node_version') or DEFAULT_NODE_VERSION}",
+ f"RUN cd {local_deps.working_dir} && {_get_node_pm_install_cmd(config_path, config)} && tsx /api/langgraph_api/js/build.mts",
+ "# -- End of JS dependencies install --",
+ ]
+ )
+ image_str = docker_tag(config, base_image, api_version)
+
+ # Prepare docker file contents
+ docker_file_contents = []
+
+ # Add syntax directive if we have additional contexts (requires BuildKit frontend.contexts capability)
+ if local_deps.additional_contexts:
+ docker_file_contents.extend(
+ [
+ "# syntax=docker/dockerfile:1.4",
+ "",
+ ]
+ )
+
+ # Add main dockerfile content
+ dep_vname = "$$dep" if escape_variables else "$dep"
+ docker_file_contents.extend(
+ [
+ f"FROM {image_str}",
+ "",
+ os.linesep.join(config["dockerfile_lines"]),
+ "",
+ installs,
+ "",
+ "# -- Installing all local dependencies --",
+ f"""RUN for dep in /deps/*; do \
+ echo "Installing {dep_vname}"; \
+ if [ -d "{dep_vname}" ]; then \
+ echo "Installing {dep_vname}"; \
+ (cd "{dep_vname}" && {global_reqs_pip_install} -e .); \
+ fi; \
+ done""",
+ "# -- End of local dependencies install --",
+ os.linesep.join(env_vars),
+ "",
+ js_inst_str,
+ "",
+ # Add pip cleanup after all installations are complete
+ _get_pip_cleanup_lines(
+ install_cmd=install_cmd,
+ to_uninstall=build_tools_to_uninstall,
+ pip_installer=pip_installer,
+ ),
+ "",
+ f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "",
+ ]
+ )
+
+ additional_contexts: dict[str, str] = {}
+ for p in local_deps.additional_contexts:
+ if p in local_deps.real_pkgs:
+ name = local_deps.real_pkgs[p][1]
+ elif p in local_deps.faux_pkgs:
+ name = f"outer-{p.name}"
+ else:
+ raise RuntimeError(f"Unknown additional context: {p}")
+ additional_contexts[name] = str(p)
+
+ return os.linesep.join(docker_file_contents), additional_contexts
+
+
+def node_config_to_docker(
+ config_path: pathlib.Path,
+ config: Config,
+ base_image: str,
+ api_version: str | None = None,
+ install_command: str | None = None,
+ build_command: str | None = None,
+ build_context: str | None = None,
+) -> tuple[str, dict[str, str]]:
+ # Calculate paths for monorepo support
+ if build_context:
+ relative_workdir = _calculate_relative_workdir(config_path, build_context)
+ container_name = pathlib.Path(build_context).name
+ if relative_workdir:
+ faux_path = f"/deps/{container_name}/{relative_workdir}"
+ else:
+ faux_path = f"/deps/{container_name}"
+ else:
+ # Backward compatibility: use the original behavior
+ faux_path = f"/deps/{config_path.parent.name}"
+
+ # Use custom install command or auto-detect
+ if install_command:
+ install_cmd = install_command
+ else:
+ install_cmd = _get_node_pm_install_cmd(config_path, config)
+
+ image_str = docker_tag(config, base_image, api_version)
+
+ env_vars: list[str] = []
+
+ if (store_config := config.get("store")) is not None:
+ env_vars.append(f"ENV LANGGRAPH_STORE='{json.dumps(store_config)}'")
+
+ if (auth_config := config.get("auth")) is not None:
+ env_vars.append(f"ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'")
+
+ if (encryption_config := config.get("encryption")) is not None:
+ env_vars.append(f"ENV LANGGRAPH_ENCRYPTION='{json.dumps(encryption_config)}'")
+
+ if (http_config := config.get("http")) is not None:
+ env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
+
+ # Inject webhooks configuration if provided
+ if (webhooks_config := config.get("webhooks")) is not None:
+ env_vars.append(f"ENV LANGGRAPH_WEBHOOKS='{json.dumps(webhooks_config)}'")
+
+ if (checkpointer_config := config.get("checkpointer")) is not None:
+ env_vars.append(
+ f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'"
+ )
+
+ if ui := config.get("ui"):
+ env_vars.append(f"ENV LANGGRAPH_UI='{json.dumps(ui)}'")
+
+ if ui_config := config.get("ui_config"):
+ env_vars.append(f"ENV LANGGRAPH_UI_CONFIG='{json.dumps(ui_config)}'")
+
+ env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(config['graphs'])}'")
+
+ # For monorepo support, we need to handle install and build commands differently
+ if build_context:
+ # Monorepo case: install from root, build from config directory
+ container_root = f"/deps/{pathlib.Path(build_context).name}"
+ install_step = f"RUN cd {container_root} && {install_cmd}"
+
+ if build_command:
+ build_step = f"RUN cd {faux_path} && {build_command}"
+ else:
+ build_step = 'RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts'
+ else:
+ # Original behavior: everything happens in the same directory
+ install_step = f"RUN cd {faux_path} && {install_cmd}"
+ build_step = 'RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts'
+
+ docker_file_contents = [
+ f"FROM {image_str}",
+ "",
+ os.linesep.join(config["dockerfile_lines"]),
+ "",
+ f"ADD . {faux_path if not build_context else container_root}",
+ "",
+ install_step,
+ "",
+ os.linesep.join(env_vars),
+ "",
+ f"WORKDIR {faux_path}",
+ "",
+ build_step,
+ ]
+
+ return os.linesep.join(docker_file_contents), {}
+
+
+def default_base_image(config: Config) -> str:
+ if config.get("base_image"):
+ return config["base_image"]
+ if config.get("node_version") and not config.get("python_version"):
+ return "langchain/langgraphjs-api"
+ return "langchain/langgraph-api"
+
+
+def docker_tag(
+ config: Config,
+ base_image: str | None = None,
+ api_version: str | None = None,
+) -> str:
+ api_version = api_version or config.get("api_version")
+ base_image = base_image or default_base_image(config)
+
+ image_distro = config.get("image_distro")
+ distro_tag = "" if image_distro == DEFAULT_IMAGE_DISTRO else f"-{image_distro}"
+
+ if config.get("_INTERNAL_docker_tag"):
+ return f"{base_image}:{config['_INTERNAL_docker_tag']}"
+
+ # Build the standard tag format
+ language, version = None, None
+ if config.get("node_version") and not config.get("python_version"):
+ language, version = "node", config["node_version"]
+ else:
+ language, version = "py", config["python_version"]
+
+ version_distro_tag = f"{version}{distro_tag}"
+
+ # Prepend API version if provided
+ if api_version:
+ full_tag = f"{api_version}-{language}{version_distro_tag}"
+ elif "/langgraph-server" in base_image and version_distro_tag not in base_image:
+ return f"{base_image}-{language}{version_distro_tag}"
+ else:
+ full_tag = version_distro_tag
+
+ return f"{base_image}:{full_tag}"
+
+
+def _calculate_relative_workdir(config_path: pathlib.Path, build_context: str) -> str:
+ """Calculate the relative path from build context to langgraph.json directory."""
+ config_dir = config_path.parent.resolve()
+ build_context_path = pathlib.Path(build_context).resolve()
+
+ try:
+ relative_path = config_dir.relative_to(build_context_path)
+ return str(relative_path) if str(relative_path) != "." else ""
+ except ValueError as _:
+ raise ValueError(
+ f"Configuration file {config_path} is not under the build context {build_context}. "
+ f"Please run the command from a directory that contains your langgraph.json file, "
+ ) from None
+
+
+def config_to_docker(
+ config_path: pathlib.Path,
+ config: Config,
+ *,
+ base_image: str | None = None,
+ api_version: str | None = None,
+ install_command: str | None = None,
+ build_command: str | None = None,
+ build_context: str | None = None,
+ escape_variables: bool = False,
+) -> tuple[str, dict[str, str]]:
+ base_image = base_image or default_base_image(config)
+
+ if config.get("node_version") and not config.get("python_version"):
+ return node_config_to_docker(
+ config_path=config_path,
+ config=config,
+ base_image=base_image,
+ api_version=api_version,
+ install_command=install_command,
+ build_command=build_command,
+ build_context=build_context,
+ )
+
+ return python_config_to_docker(
+ config_path=config_path,
+ config=config,
+ base_image=base_image,
+ api_version=api_version,
+ escape_variables=escape_variables,
+ )
+
+
+def config_to_compose(
+ config_path: pathlib.Path,
+ config: Config,
+ base_image: str | None = None,
+ api_version: str | None = None,
+ image: str | None = None,
+ watch: bool = False,
+) -> str:
+ base_image = base_image or default_base_image(config)
+
+ env_vars = config["env"].items() if isinstance(config["env"], dict) else {}
+ env_vars_str = "\n".join(f' {k}: "{v}"' for k, v in env_vars)
+ env_file_str = (
+ f"env_file: {config['env']}" if isinstance(config["env"], str) else ""
+ )
+ if watch:
+ dependencies = config.get("dependencies") or ["."]
+ watch_paths = [config_path.name] + [
+ dep for dep in dependencies if dep.startswith(".")
+ ]
+ watch_actions = "\n".join(
+ f"""- path: {path}
+ action: rebuild"""
+ for path in watch_paths
+ )
+ watch_str = f"""
+ develop:
+ watch:
+{textwrap.indent(watch_actions, " ")}
+"""
+ else:
+ watch_str = ""
+ if image:
+ return f"""
+{textwrap.indent(env_vars_str, " ")}
+ {env_file_str}
+ {watch_str}
+"""
+
+ else:
+ dockerfile, additional_contexts = config_to_docker(
+ config_path=config_path,
+ config=config,
+ base_image=base_image,
+ api_version=api_version,
+ escape_variables=True,
+ )
+
+ additional_contexts_str = "\n".join(
+ f" - {name}: {path}"
+ for name, path in additional_contexts.items()
+ )
+ if additional_contexts_str:
+ additional_contexts_str = f"""
+ additional_contexts:
+{additional_contexts_str}"""
+
+ return f"""
+{textwrap.indent(env_vars_str, " ")}
+ {env_file_str}
+ pull_policy: build
+ build:
+ context: .{additional_contexts_str}
+ dockerfile_inline: |
+{textwrap.indent(dockerfile, " ")}
+ {watch_str}
+"""
diff --git a/langgraph/source/libs/cli/langgraph_cli/constants.py b/langgraph/source/libs/cli/langgraph_cli/constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..117062a970f8c973df15450ac37f82bd9fb57de3
--- /dev/null
+++ b/langgraph/source/libs/cli/langgraph_cli/constants.py
@@ -0,0 +1,6 @@
+DEFAULT_CONFIG = "langgraph.json"
+DEFAULT_PORT = 8123
+
+# analytics
+SUPABASE_PUBLIC_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imt6cmxwcG9qaW5wY3l5YWlweG5iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTkyNTc1NzksImV4cCI6MjAzNDgzMzU3OX0.kkVOlLz3BxemA5nP-vat3K4qRtrDuO4SwZSR_htcX9c"
+SUPABASE_URL = "https://kzrlppojinpcyyaipxnb.supabase.co"
diff --git a/langgraph/source/libs/cli/langgraph_cli/docker.py b/langgraph/source/libs/cli/langgraph_cli/docker.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f55f34845e659d4171a33a86d073414d31de5b3
--- /dev/null
+++ b/langgraph/source/libs/cli/langgraph_cli/docker.py
@@ -0,0 +1,271 @@
+import json
+import pathlib
+import shutil
+from typing import Literal, NamedTuple
+
+import click.exceptions
+
+from langgraph_cli.exec import subp_exec
+
+ROOT = pathlib.Path(__file__).parent.resolve()
+DEFAULT_POSTGRES_URI = (
+ "postgres://postgres:postgres@langgraph-postgres:5432/postgres?sslmode=disable"
+)
+
+
+class Version(NamedTuple):
+ major: int
+ minor: int
+ patch: int
+
+
+DockerComposeType = Literal["plugin", "standalone"]
+
+
+class DockerCapabilities(NamedTuple):
+ version_docker: Version
+ version_compose: Version
+ healthcheck_start_interval: bool
+ compose_type: DockerComposeType = "plugin"
+
+
+def _parse_version(version: str) -> Version:
+ parts = version.split(".", 2)
+ if len(parts) == 1:
+ major = parts[0]
+ minor = "0"
+ patch = "0"
+ elif len(parts) == 2:
+ major, minor = parts
+ patch = "0"
+ else:
+ major, minor, patch = parts
+ return Version(
+ int(major.lstrip("v")), int(minor), int(patch.split("-")[0].split("+")[0])
+ )
+
+
+def check_capabilities(runner) -> DockerCapabilities:
+ # check docker available
+ if shutil.which("docker") is None:
+ raise click.UsageError("Docker not installed") from None
+
+ try:
+ stdout, _ = runner.run(
+ subp_exec("docker", "info", "-f", "{{json .}}", collect=True)
+ )
+ info = json.loads(stdout)
+ except (click.exceptions.Exit, json.JSONDecodeError):
+ raise click.UsageError("Docker not installed or not running") from None
+
+ if not info["ServerVersion"]:
+ raise click.UsageError("Docker not running") from None
+
+ compose_type: DockerComposeType
+ try:
+ compose = next(
+ p for p in info["ClientInfo"]["Plugins"] if p["Name"] == "compose"
+ )
+ compose_version_str = compose["Version"]
+ compose_type = "plugin"
+ except (KeyError, StopIteration):
+ if shutil.which("docker-compose") is None:
+ raise click.UsageError("Docker Compose not installed") from None
+
+ compose_version_str, _ = runner.run(
+ subp_exec("docker-compose", "--version", "--short", collect=True)
+ )
+ compose_type = "standalone"
+
+ # parse versions
+ docker_version = _parse_version(info["ServerVersion"])
+ compose_version = _parse_version(compose_version_str)
+
+ # check capabilities
+ return DockerCapabilities(
+ version_docker=docker_version,
+ version_compose=compose_version,
+ healthcheck_start_interval=docker_version >= Version(25, 0, 0),
+ compose_type=compose_type,
+ )
+
+
+def debugger_compose(*, port: int | None = None, base_url: str | None = None) -> dict:
+ if port is None:
+ return ""
+
+ config = {
+ "langgraph-debugger": {
+ "image": "langchain/langgraph-debugger",
+ "restart": "on-failure",
+ "depends_on": {
+ "langgraph-postgres": {"condition": "service_healthy"},
+ },
+ "ports": [f'"{port}:3968"'],
+ }
+ }
+
+ if base_url:
+ config["langgraph-debugger"]["environment"] = {
+ "VITE_STUDIO_LOCAL_GRAPH_URL": base_url
+ }
+
+ return config
+
+
+# Function to convert dictionary to YAML
+def dict_to_yaml(d: dict, *, indent: int = 0) -> str:
+ """Convert a dictionary to a YAML string."""
+ yaml_str = ""
+
+ for idx, (key, value) in enumerate(d.items()):
+ # Format things in a visually appealing way
+ # Use an extra newline for top-level keys only
+ if idx >= 1 and indent < 2:
+ yaml_str += "\n"
+ space = " " * indent
+ if isinstance(value, dict):
+ yaml_str += f"{space}{key}:\n" + dict_to_yaml(value, indent=indent + 1)
+ elif isinstance(value, list):
+ yaml_str += f"{space}{key}:\n"
+ for item in value:
+ yaml_str += f"{space} - {item}\n"
+ else:
+ yaml_str += f"{space}{key}: {value}\n"
+ return yaml_str
+
+
+def compose_as_dict(
+ capabilities: DockerCapabilities,
+ *,
+ port: int,
+ debugger_port: int | None = None,
+ debugger_base_url: str | None = None,
+ # postgres://user:password@host:port/database?option=value
+ postgres_uri: str | None = None,
+ # If you are running against an already-built image, you can pass it here
+ image: str | None = None,
+ # Base image to use for the LangGraph API server
+ base_image: str | None = None,
+ # API version of the base image
+ api_version: str | None = None,
+) -> dict:
+ """Create a docker compose file as a dictionary in YML style."""
+ if postgres_uri is None:
+ include_db = True
+ postgres_uri = DEFAULT_POSTGRES_URI
+ else:
+ include_db = False
+
+ # The services below are defined in a non-intuitive order to match
+ # the existing unit tests for this function.
+ # It's fine to re-order just requires updating the unit tests, so it should
+ # be done with caution.
+
+ # Define the Redis service first as per the test order
+ services = {
+ "langgraph-redis": {
+ "image": "redis:6",
+ "healthcheck": {
+ "test": "redis-cli ping",
+ "interval": "5s",
+ "timeout": "1s",
+ "retries": 5,
+ },
+ }
+ }
+
+ # Add Postgres service before langgraph-api if it is needed
+ if include_db:
+ services["langgraph-postgres"] = {
+ "image": "pgvector/pgvector:pg16",
+ "ports": ['"5433:5432"'],
+ "environment": {
+ "POSTGRES_DB": "postgres",
+ "POSTGRES_USER": "postgres",
+ "POSTGRES_PASSWORD": "postgres",
+ },
+ "command": ["postgres", "-c", "shared_preload_libraries=vector"],
+ "volumes": ["langgraph-data:/var/lib/postgresql/data"],
+ "healthcheck": {
+ "test": "pg_isready -U postgres",
+ "start_period": "10s",
+ "timeout": "1s",
+ "retries": 5,
+ },
+ }
+ if capabilities.healthcheck_start_interval:
+ services["langgraph-postgres"]["healthcheck"]["interval"] = "60s"
+ services["langgraph-postgres"]["healthcheck"]["start_interval"] = "1s"
+ else:
+ services["langgraph-postgres"]["healthcheck"]["interval"] = "5s"
+
+ # Add optional debugger service if debugger_port is specified
+ if debugger_port:
+ services["langgraph-debugger"] = debugger_compose(
+ port=debugger_port, base_url=debugger_base_url
+ )["langgraph-debugger"]
+
+ # Add langgraph-api service
+ services["langgraph-api"] = {
+ "ports": [f'"{port}:8000"'],
+ "depends_on": {
+ "langgraph-redis": {"condition": "service_healthy"},
+ },
+ "environment": {
+ "REDIS_URI": "redis://langgraph-redis:6379",
+ "POSTGRES_URI": postgres_uri,
+ },
+ }
+ if image:
+ services["langgraph-api"]["image"] = image
+
+ # If Postgres is included, add it to the dependencies of langgraph-api
+ if include_db:
+ services["langgraph-api"]["depends_on"]["langgraph-postgres"] = {
+ "condition": "service_healthy"
+ }
+
+ # Additional healthcheck for langgraph-api if required
+ if capabilities.healthcheck_start_interval:
+ services["langgraph-api"]["healthcheck"] = {
+ "test": "python /api/healthcheck.py",
+ "interval": "60s",
+ "start_interval": "1s",
+ "start_period": "10s",
+ }
+
+ # Final compose dictionary with volumes included if needed
+ compose_dict = {}
+ if include_db:
+ compose_dict["volumes"] = {"langgraph-data": {"driver": "local"}}
+ compose_dict["services"] = services
+
+ return compose_dict
+
+
+def compose(
+ capabilities: DockerCapabilities,
+ *,
+ port: int,
+ debugger_port: int | None = None,
+ debugger_base_url: str | None = None,
+ # postgres://user:password@host:port/database?option=value
+ postgres_uri: str | None = None,
+ image: str | None = None,
+ base_image: str | None = None,
+ api_version: str | None = None,
+) -> str:
+ """Create a docker compose file as a string."""
+ compose_content = compose_as_dict(
+ capabilities,
+ port=port,
+ debugger_port=debugger_port,
+ debugger_base_url=debugger_base_url,
+ postgres_uri=postgres_uri,
+ image=image,
+ base_image=base_image,
+ api_version=api_version,
+ )
+ compose_str = dict_to_yaml(compose_content)
+ return compose_str
diff --git a/langgraph/source/libs/cli/langgraph_cli/exec.py b/langgraph/source/libs/cli/langgraph_cli/exec.py
new file mode 100644
index 0000000000000000000000000000000000000000..974fc75f3905d5ce50d1839421be831ef9cbb312
--- /dev/null
+++ b/langgraph/source/libs/cli/langgraph_cli/exec.py
@@ -0,0 +1,174 @@
+import asyncio
+import signal
+import sys
+from collections.abc import Callable
+from contextlib import contextmanager
+from typing import cast
+
+import click.exceptions
+
+
+@contextmanager
+def Runner():
+ if hasattr(asyncio, "Runner"):
+ with asyncio.Runner() as runner:
+ yield runner
+ else:
+
+ class _Runner:
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ pass
+
+ def run(self, coro):
+ return asyncio.run(coro)
+
+ yield _Runner()
+
+
+async def subp_exec(
+ cmd: str,
+ *args: str,
+ input: str | None = None,
+ wait: float | None = None,
+ verbose: bool = False,
+ collect: bool = False,
+ on_stdout: Callable[[str], bool | None] | None = None,
+) -> tuple[str | None, str | None]:
+ if verbose:
+ cmd_str = f"+ {cmd} {' '.join(map(str, args))}"
+ if input:
+ print(cmd_str, " <\n", "\n".join(filter(None, input.splitlines())), sep="")
+ else:
+ print(cmd_str)
+ if wait:
+ await asyncio.sleep(wait)
+
+ try:
+ proc = await asyncio.create_subprocess_exec(
+ cmd,
+ *args,
+ stdin=asyncio.subprocess.PIPE if input else None,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE,
+ )
+
+ def signal_handler():
+ # make sure process exists, then terminate it
+ if proc.returncode is None:
+ proc.terminate()
+
+ original_sigint_handler = signal.getsignal(signal.SIGINT)
+ if sys.platform == "win32":
+
+ def handle_windows_signal(signum, frame):
+ signal_handler()
+ original_sigint_handler(signum, frame)
+
+ signal.signal(signal.SIGINT, handle_windows_signal)
+ # NOTE: we're not adding a handler for SIGTERM since it's ignored on Windows
+ else:
+ loop = asyncio.get_event_loop()
+ loop.add_signal_handler(signal.SIGINT, signal_handler)
+ loop.add_signal_handler(signal.SIGTERM, signal_handler)
+
+ empty_fut: asyncio.Future = asyncio.Future()
+ empty_fut.set_result(None)
+ stdout, stderr, _ = await asyncio.gather(
+ monitor_stream(
+ cast(asyncio.StreamReader, proc.stdout),
+ collect=True,
+ display=verbose,
+ on_line=on_stdout,
+ ),
+ monitor_stream(
+ cast(asyncio.StreamReader, proc.stderr),
+ collect=True,
+ display=verbose,
+ ),
+ proc._feed_stdin(input.encode()) if input else empty_fut, # type: ignore[attr-defined]
+ )
+ returncode = await proc.wait()
+ if (
+ returncode is not None
+ and returncode != 0 # success
+ and returncode != 130 # user interrupt
+ ):
+ sys.stdout.write(stdout.decode() if stdout else "")
+ sys.stderr.write(stderr.decode() if stderr else "")
+ raise click.exceptions.Exit(returncode)
+ if collect:
+ return (
+ stdout.decode() if stdout else None,
+ stderr.decode() if stderr else None,
+ )
+ else:
+ return None, None
+ finally:
+ try:
+ if proc.returncode is None:
+ try:
+ proc.terminate()
+ except (ProcessLookupError, KeyboardInterrupt):
+ pass
+
+ if sys.platform == "win32":
+ signal.signal(signal.SIGINT, original_sigint_handler)
+ else:
+ loop.remove_signal_handler(signal.SIGINT)
+ loop.remove_signal_handler(signal.SIGTERM)
+ except UnboundLocalError:
+ pass
+
+
+async def monitor_stream(
+ stream: asyncio.StreamReader,
+ collect: bool = False,
+ display: bool = False,
+ on_line: Callable[[str], bool | None] | None = None,
+) -> bytearray | None:
+ if collect:
+ ba = bytearray()
+
+ def handle(line: bytes, overrun: bool):
+ nonlocal on_line
+ nonlocal display
+
+ if display:
+ sys.stdout.buffer.write(line)
+ if overrun:
+ return
+ if collect:
+ ba.extend(line)
+ if on_line:
+ if on_line(line.decode()):
+ on_line = None
+ display = True
+
+ """Adapted from asyncio.StreamReader.readline() to handle LimitOverrunError."""
+ sep = b"\n"
+ seplen = len(sep)
+ while True:
+ try:
+ line = await stream.readuntil(sep)
+ overrun = False
+ except asyncio.IncompleteReadError as e:
+ line = e.partial
+ overrun = False
+ except asyncio.LimitOverrunError as e:
+ if stream._buffer.startswith(sep, e.consumed):
+ line = stream._buffer[: e.consumed + seplen]
+ else:
+ line = stream._buffer.clear()
+ overrun = True
+ stream._maybe_resume_transport()
+ await asyncio.to_thread(handle, line, overrun)
+ if line == b"":
+ break
+
+ if collect:
+ return ba
+ else:
+ return None
diff --git a/langgraph/source/libs/cli/langgraph_cli/progress.py b/langgraph/source/libs/cli/langgraph_cli/progress.py
new file mode 100644
index 0000000000000000000000000000000000000000..5c3dabded6c008537cf3928e15026af31b7858e8
--- /dev/null
+++ b/langgraph/source/libs/cli/langgraph_cli/progress.py
@@ -0,0 +1,73 @@
+import sys
+import threading
+import time
+from collections.abc import Callable
+
+
+class Progress:
+ delay: float = 0.1
+
+ @staticmethod
+ def spinning_cursor():
+ while True:
+ yield from "|/-\\"
+
+ def __init__(self, *, message=""):
+ self.message = message
+ self.spinner_generator = self.spinning_cursor()
+
+ def spinner_iteration(self):
+ message = self.message
+ sys.stdout.write(next(self.spinner_generator) + " " + message)
+ sys.stdout.flush()
+ time.sleep(self.delay)
+ # clear the spinner and message
+ sys.stdout.write(
+ "\b" * (len(message) + 2)
+ + " " * (len(message) + 2)
+ + "\b" * (len(message) + 2)
+ )
+ sys.stdout.flush()
+
+ def spinner_task(self):
+ while self.message:
+ message = self.message
+ sys.stdout.write(next(self.spinner_generator) + " " + message)
+ sys.stdout.flush()
+ time.sleep(self.delay)
+ # clear the spinner and message
+ sys.stdout.write(
+ "\b" * (len(message) + 2)
+ + " " * (len(message) + 2)
+ + "\b" * (len(message) + 2)
+ )
+ sys.stdout.flush()
+
+ def __enter__(self) -> Callable[[str], None]:
+ if sys.stdout.isatty():
+ self.thread = threading.Thread(target=self.spinner_task)
+ self.thread.start()
+
+ def set_message(message):
+ self.message = message
+ if not message:
+ self.thread.join()
+
+ return set_message
+ else:
+
+ def set_message(message):
+ sys.stderr.write(message + "\n")
+ sys.stderr.flush()
+
+ return set_message
+
+ def __exit__(self, exception, value, tb):
+ if sys.stdout.isatty():
+ self.message = ""
+ try:
+ self.thread.join()
+ finally:
+ del self.thread
+ if exception is not None:
+ return False
diff --git a/langgraph/source/libs/cli/langgraph_cli/py.typed b/langgraph/source/libs/cli/langgraph_cli/py.typed
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/langgraph_cli/schemas.py b/langgraph/source/libs/cli/langgraph_cli/schemas.py
new file mode 100644
index 0000000000000000000000000000000000000000..fb517becd530470409f2a80eee85d3eb37d55b6c
--- /dev/null
+++ b/langgraph/source/libs/cli/langgraph_cli/schemas.py
@@ -0,0 +1,688 @@
+from typing import Any, Literal, TypedDict
+
+Distros = Literal["debian", "wolfi", "bullseye", "bookworm"]
+MiddlewareOrders = Literal["auth_first", "middleware_first"]
+
+
+class TTLConfig(TypedDict, total=False):
+ """Configuration for TTL (time-to-live) behavior in the store."""
+
+ refresh_on_read: bool
+ """Default behavior for refreshing TTLs on read operations (`GET` and `SEARCH`).
+
+ If `True`, TTLs will be refreshed on read operations (get/search) by default.
+ This can be overridden per-operation by explicitly setting `refresh_ttl`.
+ Defaults to `True` if not configured.
+ """
+ default_ttl: float | None
+ """Optional. Default TTL (time-to-live) in minutes for new items.
+
+ If provided, all new items will have this TTL unless explicitly overridden.
+ If omitted, items will have no TTL by default.
+ """
+ sweep_interval_minutes: int | None
+ """Optional. Interval in minutes between TTL sweep iterations.
+
+ If provided, the store will periodically delete expired items based on the TTL.
+ If omitted, no automatic sweeping will occur.
+ """
+
+
+class IndexConfig(TypedDict, total=False):
+ """Configuration for indexing documents for semantic search in the store.
+
+ This governs how text is converted into embeddings and stored for vector-based lookups.
+ """
+
+ dims: int
+ """Required. Dimensionality of the embedding vectors you will store.
+
+ Must match the output dimension of your selected embedding model or custom embed function.
+ If mismatched, you will likely encounter shape/size errors when inserting or querying vectors.
+
+ Common embedding model output 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
+ """
+
+ embed: str
+ """Required. Identifier or reference to the embedding model or a custom embedding function.
+
+ The format can vary:
+ - ":" for recognized providers (e.g., "openai:text-embedding-3-large")
+ - "path/to/module.py:function_name" for your own local embedding function
+ - "my_custom_embed" if it's a known alias in your system
+
+ Examples:
+ - "openai:text-embedding-3-large"
+ - "cohere:embed-multilingual-v3.0"
+ - "src/app.py:embeddings"
+
+ Note: Must return embeddings of dimension `dims`.
+ """
+
+ fields: list[str] | None
+ """Optional. List of JSON fields to extract before generating embeddings.
+
+ Defaults to ["$"], which means the entire JSON object is embedded as one piece of text.
+ If you provide multiple fields (e.g. ["title", "content"]), each is extracted and embedded separately,
+ often saving token usage if you only care about certain parts of the data.
+
+ Example:
+ fields=["title", "abstract", "author.biography"]
+ """
+
+
+class StoreConfig(TypedDict, total=False):
+ """Configuration for the built-in long-term memory store.
+
+ This store can optionally perform semantic search. If you omit `index`,
+ the store will just handle traditional (non-embedded) data without vector lookups.
+ """
+
+ index: IndexConfig | None
+ """Optional. Defines the vector-based semantic search configuration.
+
+ If provided, the store will:
+ - Generate embeddings according to `index.embed`
+ - Enforce the embedding dimension given by `index.dims`
+ - Embed only specified JSON fields (if any) from `index.fields`
+
+ If omitted, no vector index is initialized.
+ """
+
+ ttl: TTLConfig | None
+ """Optional. Defines the TTL (time-to-live) behavior configuration.
+
+ If provided, the store will apply TTL settings according to the configuration.
+ If omitted, no TTL behavior is configured.
+ """
+
+
+class ThreadTTLConfig(TypedDict, total=False):
+ """Configure a default TTL for checkpointed data within threads."""
+
+ strategy: Literal["delete"]
+ """Strategy to use for deleting checkpointed data.
+
+ Choices:
+ - "delete": Delete all checkpoints for a thread after TTL expires.
+ """
+ default_ttl: float | None
+ """Default TTL (time-to-live) in minutes for checkpointed data."""
+ sweep_interval_minutes: int | None
+ """Interval in minutes between sweep iterations.
+ If omitted, a default interval will be used (typically ~ 5 minutes)."""
+
+
+class SerdeConfig(TypedDict, total=False):
+ """Configuration for the built-in serde, which handles checkpointing of state.
+
+ If omitted, no serde is set up (the object store will still be present, however)."""
+
+ allowed_json_modules: list[list[str]] | bool | None
+ """Optional. List of allowed python modules to de-serialize custom objects from.
+
+ If provided, only the specified modules will be allowed to be deserialized.
+ If omitted, no modules are allowed, and the object returned will simply be a json object OR
+ a deserialized langchain object.
+
+ Example:
+ {...
+ "serde": {
+ "allowed_json_modules": [
+ ["my_agent", "my_file", "SomeType"],
+ ]
+ }
+ }
+
+ If you set this to True, any module will be allowed to be deserialized.
+
+ Example:
+ {...
+ "serde": {
+ "allowed_json_modules": true
+ }
+ }
+
+ """
+ pickle_fallback: bool
+ """Optional. Whether to allow pickling as a fallback for deserialization.
+
+ If True, pickling will be allowed as a fallback for deserialization.
+ If False, pickling will not be allowed as a fallback for deserialization.
+ Defaults to True if not configured."""
+
+
+class CheckpointerConfig(TypedDict, total=False):
+ """Configuration for the built-in checkpointer, which handles checkpointing of state.
+
+ If omitted, no checkpointer is set up (the object store will still be present, however).
+ """
+
+ ttl: ThreadTTLConfig | None
+ """Optional. Defines the TTL (time-to-live) behavior configuration.
+
+ If provided, the checkpointer will apply TTL settings according to the configuration.
+ If omitted, no TTL behavior is configured.
+ """
+ serde: SerdeConfig | None
+ """Optional. Defines the serde configuration.
+
+ If provided, the checkpointer will apply serde settings according to the configuration.
+ If omitted, no serde behavior is configured.
+
+ This configuration requires server version 0.5 or later to take effect.
+ """
+ sweep_limit: int | None
+ """Maximum number of threads to process per sweep iteration. Defaults to 1000."""
+
+
+class SecurityConfig(TypedDict, total=False):
+ """Configuration for OpenAPI security definitions and requirements.
+
+ Useful for specifying global or path-level authentication and authorization flows
+ (e.g., OAuth2, API key headers, etc.).
+ """
+
+ securitySchemes: dict[str, dict[str, Any]]
+ """Describe each security scheme recognized by your OpenAPI spec.
+
+ Keys are scheme names (e.g. "OAuth2", "ApiKeyAuth") and values are their definitions.
+ Example:
+ {
+ "OAuth2": {
+ "type": "oauth2",
+ "flows": {
+ "password": {
+ "tokenUrl": "/token",
+ "scopes": {"read": "Read data", "write": "Write data"}
+ }
+ }
+ }
+ }
+ """
+ security: list[dict[str, list[str]]]
+ """Global security requirements across all endpoints.
+
+ Each element in the list maps a security scheme (e.g. "OAuth2") to a list of scopes (e.g. ["read", "write"]).
+ Example:
+ [
+ {"OAuth2": ["read", "write"]},
+ {"ApiKeyAuth": []}
+ ]
+ """
+ # path => {method => security}
+ paths: dict[str, dict[str, list[dict[str, list[str]]]]]
+ """Path-specific security overrides.
+
+ Keys are path templates (e.g., "/items/{item_id}"), mapping to:
+ - Keys that are HTTP methods (e.g., "GET", "POST"),
+ - Values are lists of security definitions (just like `security`) for that method.
+
+ Example:
+ {
+ "/private_data": {
+ "GET": [{"OAuth2": ["read"]}],
+ "POST": [{"OAuth2": ["write"]}]
+ }
+ }
+ """
+
+
+class CacheConfig(TypedDict, total=False):
+ cache_keys: list[str]
+ """Optional. List of header keys to use for caching.
+
+ Example:
+ ["user_id", "workspace_id"]
+ """
+ ttl_seconds: int
+ """Optional. Time-to-live in seconds for cached items.
+
+ Example:
+ 3600
+ """
+ max_size: int
+ """Optional. Maximum size of the cache.
+
+ Example:
+ 100
+ """
+
+
+class AuthConfig(TypedDict, total=False):
+ """Configuration for custom authentication logic and how it integrates into the OpenAPI spec."""
+
+ path: str
+ """Required. Path to an instance of the Auth() class that implements custom authentication.
+
+ Format: "path/to/file.py:my_auth"
+ """
+ disable_studio_auth: bool
+ """Optional. Whether to disable LangSmith API-key authentication for requests originating the Studio.
+
+ Defaults to False, meaning that if a particular header is set, the server will verify the `x-api-key` header
+ value is a valid API key for the deployment's workspace. If `True`, all requests will go through your custom
+ authentication logic, regardless of origin of the request.
+ """
+ openapi: SecurityConfig
+ """The security configuration to include in your server's OpenAPI spec.
+
+ Example (OAuth2):
+ {
+ "securitySchemes": {
+ "OAuth2": {
+ "type": "oauth2",
+ "flows": {
+ "password": {
+ "tokenUrl": "/token",
+ "scopes": {"me": "Read user info", "items": "Manage items"}
+ }
+ }
+ }
+ },
+ "security": [
+ {"OAuth2": ["me"]}
+ ]
+ }
+ """
+ cache: CacheConfig
+ """Optional. Cache configuration for the server.
+
+ Example:
+ {
+ "cache_keys": ["user_id", "workspace_id"],
+ "ttl_seconds": 3600,
+ "max_size": 100
+ }
+ """
+
+
+class EncryptionConfig(TypedDict, total=False):
+ """Configuration for custom at-rest encryption logic.
+
+ Allows you to implement custom encryption for sensitive data stored in the database,
+ including metadata fields and checkpoint blobs.
+ """
+
+ path: str
+ """Required. Path to an instance of the Encryption() class that implements custom encryption handlers.
+
+ Format: "path/to/file.py:my_encryption"
+
+ Example:
+ {
+ "encryption": {
+ "path": "./encryption.py:my_encryption"
+ }
+ }
+ """
+
+
+class CorsConfig(TypedDict, total=False):
+ """Specifies Cross-Origin Resource Sharing (CORS) rules for your server.
+
+ If omitted, defaults are typically very restrictive (often no cross-origin requests).
+ Configure carefully if you want to allow usage from browsers hosted on other domains.
+ """
+
+ allow_origins: list[str]
+ """Optional. List of allowed origins (e.g., "https://example.com").
+
+ Default is often an empty list (no external origins).
+ Use "*" only if you trust all origins, as that bypasses most restrictions.
+ """
+ allow_methods: list[str]
+ """Optional. HTTP methods permitted for cross-origin requests (e.g. ["GET", "POST"]).
+
+ Default might be ["GET", "POST", "OPTIONS"] depending on your server framework.
+ """
+ allow_headers: list[str]
+ """Optional. HTTP headers that can be used in cross-origin requests (e.g. ["Content-Type", "Authorization"])."""
+ allow_credentials: bool
+ """Optional. If `True`, cross-origin requests can include credentials (cookies, auth headers).
+
+ Default False to avoid accidentally exposing secured endpoints to untrusted sites.
+ """
+ allow_origin_regex: str
+ """Optional. A regex pattern for matching allowed origins, used if you have dynamic subdomains.
+
+ Example: "^https://.*\\.mycompany\\.com$"
+ """
+ expose_headers: list[str]
+ """Optional. List of headers that browsers are allowed to read from the response in cross-origin contexts."""
+ max_age: int
+ """Optional. How many seconds the browser may cache preflight responses.
+
+ Default might be 600 (10 minutes). Larger values reduce preflight requests but can cause stale configurations.
+ """
+
+
+class ConfigurableHeaderConfig(TypedDict, total=False):
+ """Customize which headers to include as configurable values in your runs.
+
+ By default, omits x-api-key, x-tenant-id, and x-service-key.
+
+ Exclusions (if provided) take precedence.
+
+ Each value can be a raw string with an optional wildcard.
+ """
+
+ includes: list[str] | None
+ """Headers to include (if not also matched against an 'excludes' pattern).
+
+ Examples:
+ - 'user-agent'
+ - 'x-configurable-*'
+ """
+ excludes: list[str] | None
+ """Headers to exclude. Applied before the 'includes' checks.
+
+ Examples:
+ - 'x-api-key'
+ - '*key*'
+ - '*token*'
+ """
+
+
+class HttpConfig(TypedDict, total=False):
+ """Configuration for the built-in HTTP server that powers your deployment's routes and endpoints."""
+
+ app: str
+ """Optional. Import path to a custom Starlette/FastAPI application to mount.
+
+ Format: "path/to/module.py:app_var"
+ If provided, it can override or extend the default routes.
+ """
+ disable_assistants: bool
+ """Optional. If `True`, /assistants routes are removed from the server.
+
+ Default is False (meaning /assistants is enabled).
+ """
+ disable_threads: bool
+ """Optional. If `True`, /threads routes are removed.
+
+ Default is False.
+ """
+ disable_runs: bool
+ """Optional. If `True`, /runs routes are removed.
+
+ Default is False.
+ """
+ disable_store: bool
+ """Optional. If `True`, /store routes are removed, disabling direct store interactions via HTTP.
+
+ Default is False.
+ """
+ disable_mcp: bool
+ """Optional. If `True`, /mcp routes are removed, disabling default support to expose the deployment as an MCP server.
+
+ Default is False.
+ """
+ disable_a2a: bool
+ """Optional. If `True`, /a2a routes are removed, disabling default support to expose the deployment as an agent-to-agent (A2A) server.
+
+ Default is False.
+ """
+ disable_meta: bool
+ """Optional. Remove meta endpoints.
+
+ Set to True to disable the following endpoints: /openapi.json, /info, /metrics, /docs.
+ This will also make the /ok endpoint skip any DB or other checks, always returning {"ok": True}.
+
+ Default is False.
+ """
+ disable_ui: bool
+ """Optional. If `True`, /ui routes are removed, disabling the UI server.
+
+ Default is False.
+ """
+ disable_webhooks: bool
+ """Optional. If `True`, webhooks are disabled. Runs created with an associated webhook will
+ still be executed, but the webhook event will not be sent.
+
+ Default is False.
+ """
+ cors: CorsConfig | None
+ """Optional. Defines CORS restrictions. If omitted, no special rules are set and
+ cross-origin behavior depends on default server settings.
+ """
+ configurable_headers: ConfigurableHeaderConfig | None
+ """Optional. Defines how headers are treated for a run's configuration.
+
+ You can include or exclude headers as configurable values to condition your
+ agent's behavior or permissions on a request's headers."""
+ logging_headers: ConfigurableHeaderConfig | None
+ """Optional. Defines which headers are excluded from logging."""
+ middleware_order: MiddlewareOrders | None
+ """Optional. Defines the order in which to apply server customizations.
+
+ Choices:
+ - "auth_first": Authentication hooks (custom or default) are evaluated
+ before custom middleware.
+ - "middleware_first": Custom middleware is evaluated
+ before authentication hooks (custom or default).
+
+ Default is `middleware_first`.
+ """
+ enable_custom_route_auth: bool
+ """Optional. If `True`, authentication is enabled for custom routes,
+ not just the routes that are protected by default.
+ (Routes protected by default include /assistants, /threads, and /runs).
+
+ Default is False. This flag only affects authentication behavior
+ if `app` is provided and contains custom routes.
+ """
+ mount_prefix: str
+ """Optional. URL prefix to prepend to all the routes.
+
+ Example:
+ "/api"
+ """
+
+
+class WebhookUrlPolicy(TypedDict, total=False):
+ require_https: bool
+ """Enforce HTTPS scheme for absolute URLs; reject `http://` when true."""
+ allowed_domains: list[str]
+ """Hostname allowlist. Supports exact hosts and wildcard subdomains.
+
+ Use entries like "hooks.example.com" or "*.mycorp.com". The wildcard only
+ matches subdomains ("foo.mycorp.com"), not the apex ("mycorp.com"). When
+ empty or omitted, any public host is allowed (subject to SSRF IP checks).
+ """
+ allowed_ports: list[int]
+ """Explicit port allowlist for absolute URLs.
+
+ If set, requests must use one of these ports. Defaults are respected when
+ a port is not present in the URL (443 for https, 80 for http).
+ """
+ max_url_length: int
+ """Maximum permitted URL length in characters; longer inputs are rejected early."""
+ disable_loopback: bool
+ """Disallow relative URLs (internal loopback calls) when true."""
+
+
+class WebhooksConfig(TypedDict, total=False):
+ env_prefix: str
+ """Required prefix for environment variables referenced in header templates.
+
+ Acts as an allowlist boundary to prevent leaking arbitrary environment
+ variables. Defaults to "LG_WEBHOOK_" when omitted.
+ """
+ url: WebhookUrlPolicy
+ """URL validation policy for user-supplied webhook endpoints."""
+ headers: dict[str, str]
+ """Static headers to include with webhook requests.
+
+ Values may contain templates of the form "${{ env.VAR }}". On startup, these
+ are resolved via the process environment after verifying `VAR` starts with
+ `env_prefix`. Mixed literals and multiple templates are allowed.
+ """
+
+
+class Config(TypedDict, total=False):
+ """Top-level config for langgraph-cli or similar deployment tooling."""
+
+ python_version: str
+ """Optional. Python version in 'major.minor' format (e.g. '3.11').
+ Must be at least 3.11 or greater for this deployment to function properly.
+ """
+
+ node_version: str | None
+ """Optional. Node.js version as a major version (e.g. '20'), if your deployment needs Node.
+ Must be >= 20 if provided.
+ """
+
+ api_version: str | None
+ """Optional. Which semantic version of the LangGraph API server to use.
+
+ Defaults to latest. Check the
+ [changelog](https://docs.langchain.com/langgraph-platform/langgraph-server-changelog)
+ for more information."""
+
+ _INTERNAL_docker_tag: str | None
+ """Optional. Internal use only.
+ """
+
+ base_image: str | None
+ """Optional. Base image to use for the LangGraph API server.
+
+ Defaults to langchain/langgraph-api or langchain/langgraphjs-api."""
+
+ image_distro: Distros | None
+ """Optional. Linux distribution for the base image.
+
+ Must be one of 'wolfi', 'debian', 'bullseye', or 'bookworm'.
+ If omitted, defaults to 'debian' ('latest').
+ """
+
+ pip_config_file: str | None
+ """Optional. Path to a pip config file (e.g., "/etc/pip.conf" or "pip.ini") for controlling
+ package installation (custom indices, credentials, etc.).
+
+ Only relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.
+ """
+
+ pip_installer: str | None
+ """Optional. Python package installer to use ('auto', 'pip', 'uv').
+
+ - 'auto' (default): Use uv for supported base images, otherwise pip
+ - 'pip': Force use of pip regardless of base image support
+ - 'uv': Force use of uv (will fail if base image doesn't support it)
+ """
+
+ dockerfile_lines: list[str]
+ """Optional. Additional Docker instructions that will be appended to your base Dockerfile.
+
+ Useful for installing OS packages, setting environment variables, etc.
+ Example:
+ dockerfile_lines=[
+ "RUN apt-get update && apt-get install -y libmagic-dev",
+ "ENV MY_CUSTOM_VAR=hello_world"
+ ]
+ """
+
+ dependencies: list[str]
+ """List of Python dependencies to install, either from PyPI or local paths.
+
+ Examples:
+ - "." or "./src" if you have a local Python package
+ - str (aka "anthropic") for a PyPI package
+ - "git+https://github.com/org/repo.git@main" for a Git-based package
+ Defaults to an empty list, meaning no additional packages installed beyond your base environment.
+ """
+
+ graphs: dict[str, str]
+ """Optional. Named definitions of graphs, each pointing to a Python object.
+
+
+ Graphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context
+ managers that accept a single configuration argument (of type RunnableConfig) and return a pregel object
+ (instance of Stategraph, etc.).
+
+ Keys are graph names, values are "path/to/file.py:object_name".
+ Example:
+ {
+ "mygraph": "graphs/my_graph.py:graph_definition",
+ "anothergraph": "graphs/another.py:get_graph"
+ }
+ """
+
+ env: dict[str, str] | str
+ """Optional. Environment variables to set for your deployment.
+
+ - If given as a dict, keys are variable names and values are their values.
+ - If given as a string, it must be a path to a file containing lines in KEY=VALUE format.
+
+ Example as a dict:
+ env={"API_TOKEN": "abc123", "DEBUG": "true"}
+ Example as a file path:
+ env=".env"
+ """
+
+ store: StoreConfig | None
+ """Optional. Configuration for the built-in long-term memory store, including semantic search indexing.
+
+ If omitted, no vector index is set up (the object store will still be present, however).
+ """
+
+ checkpointer: CheckpointerConfig | None
+ """Optional. Configuration for the built-in checkpointer, which handles checkpointing of state.
+
+ If omitted, no checkpointer is set up (the object store will still be present, however).
+ """
+
+ auth: AuthConfig | None
+ """Optional. Custom authentication config, including the path to your Python auth logic and
+ the OpenAPI security definitions it uses.
+ """
+
+ encryption: EncryptionConfig | None
+ """Optional. Custom at-rest encryption config, including the path to your Python encryption logic.
+
+ Allows you to implement custom encryption for sensitive data stored in the database.
+ """
+
+ http: HttpConfig | None
+ """Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed
+ and how cross-origin requests are handled.
+ """
+
+ webhooks: WebhooksConfig | None
+ """Optional. Webhooks configuration for outbound event delivery.
+
+ Forwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`
+ for URL policy and header templating details.
+ """
+
+ ui: dict[str, str] | None
+ """Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.
+ """
+
+ keep_pkg_tools: bool | list[str] | None
+ """Optional. Control whether to retain Python packaging tools in the final image.
+
+ Allowed tools are: "pip", "setuptools", "wheel".
+ You can also set to true to include all packaging tools.
+ """
+
+
+__all__ = [
+ "Config",
+ "StoreConfig",
+ "CheckpointerConfig",
+ "AuthConfig",
+ "EncryptionConfig",
+ "HttpConfig",
+ "MiddlewareOrders",
+ "Distros",
+ "TTLConfig",
+ "IndexConfig",
+]
diff --git a/langgraph/source/libs/cli/langgraph_cli/templates.py b/langgraph/source/libs/cli/langgraph_cli/templates.py
new file mode 100644
index 0000000000000000000000000000000000000000..a95e9dac918717cc8e6f9cccd4e2d5f5b35cb8ff
--- /dev/null
+++ b/langgraph/source/libs/cli/langgraph_cli/templates.py
@@ -0,0 +1,222 @@
+import os
+import shutil
+import sys
+from io import BytesIO
+from urllib import error, request
+from zipfile import ZipFile
+
+import click
+
+TEMPLATES: dict[str, dict[str, str]] = {
+ "New LangGraph Project": {
+ "description": "A simple, minimal chatbot with memory.",
+ "python": "https://github.com/langchain-ai/new-langgraph-project/archive/refs/heads/main.zip",
+ "js": "https://github.com/langchain-ai/new-langgraphjs-project/archive/refs/heads/main.zip",
+ },
+ "ReAct Agent": {
+ "description": "A simple agent that can be flexibly extended to many tools.",
+ "python": "https://github.com/langchain-ai/react-agent/archive/refs/heads/main.zip",
+ "js": "https://github.com/langchain-ai/react-agent-js/archive/refs/heads/main.zip",
+ },
+ "Memory Agent": {
+ "description": "A ReAct-style agent with an additional tool to store memories for use across conversational threads.",
+ "python": "https://github.com/langchain-ai/memory-agent/archive/refs/heads/main.zip",
+ "js": "https://github.com/langchain-ai/memory-agent-js/archive/refs/heads/main.zip",
+ },
+ "Retrieval Agent": {
+ "description": "An agent that includes a retrieval-based question-answering system.",
+ "python": "https://github.com/langchain-ai/retrieval-agent-template/archive/refs/heads/main.zip",
+ "js": "https://github.com/langchain-ai/retrieval-agent-template-js/archive/refs/heads/main.zip",
+ },
+ "Data-enrichment Agent": {
+ "description": "An agent that performs web searches and organizes its findings into a structured format.",
+ "python": "https://github.com/langchain-ai/data-enrichment/archive/refs/heads/main.zip",
+ "js": "https://github.com/langchain-ai/data-enrichment-js/archive/refs/heads/main.zip",
+ },
+}
+
+# Generate TEMPLATE_IDS programmatically
+TEMPLATE_ID_TO_CONFIG = {
+ f"{name.lower().replace(' ', '-')}-{lang}": (name, lang, url)
+ for name, versions in TEMPLATES.items()
+ for lang, url in versions.items()
+ if lang in {"python", "js"}
+}
+
+TEMPLATE_IDS = list(TEMPLATE_ID_TO_CONFIG.keys())
+
+TEMPLATE_HELP_STRING = (
+ "The name of the template to use. Available options:\n"
+ + "\n".join(f"{id_}" for id_ in TEMPLATE_ID_TO_CONFIG)
+)
+
+
+def _choose_template() -> str:
+ """Presents a list of templates to the user and prompts them to select one.
+
+ Returns:
+ str: The URL of the selected template.
+ """
+ click.secho("🌟 Please select a template:", bold=True, fg="yellow")
+ for idx, (template_name, template_info) in enumerate(TEMPLATES.items(), 1):
+ click.secho(f"{idx}. ", nl=False, fg="cyan")
+ click.secho(template_name, fg="cyan", nl=False)
+ click.secho(f" - {template_info['description']}", fg="white")
+
+ # Get the template choice from the user, defaulting to the first template if blank
+ template_choice: int | None = click.prompt(
+ "Enter the number of your template choice (default is 1)",
+ type=int,
+ default=1,
+ show_default=False,
+ )
+
+ template_keys = list(TEMPLATES.keys())
+ if 1 <= template_choice <= len(template_keys):
+ selected_template: str = template_keys[template_choice - 1]
+ else:
+ click.secho("❌ Invalid choice. Please try again.", fg="red")
+ return _choose_template()
+
+ # Prompt the user to choose between Python or JS/TS version
+ click.secho(
+ f"\nYou selected: {selected_template} - {TEMPLATES[selected_template]['description']}",
+ fg="green",
+ )
+ version_choice: int = click.prompt(
+ "Choose language (1 for Python 🐍, 2 for JS/TS 🌐)", type=int
+ )
+
+ if version_choice == 1:
+ return TEMPLATES[selected_template]["python"]
+ elif version_choice == 2:
+ return TEMPLATES[selected_template]["js"]
+ else:
+ click.secho("❌ Invalid choice. Please try again.", fg="red")
+ return _choose_template()
+
+
+def _download_repo_with_requests(repo_url: str, path: str) -> None:
+ """Download a ZIP archive from the given URL and extracts it to the specified path.
+
+ Args:
+ repo_url: The URL of the repository to download.
+ path: The path where the repository should be extracted.
+ """
+ click.secho("📥 Attempting to download repository as a ZIP archive...", fg="yellow")
+ click.secho(f"URL: {repo_url}", fg="yellow")
+ try:
+ with request.urlopen(repo_url) as response:
+ if response.status == 200:
+ with ZipFile(BytesIO(response.read())) as zip_file:
+ zip_file.extractall(path)
+ # Move extracted contents to path
+ for item in os.listdir(path):
+ if item.endswith("-main"):
+ extracted_dir = os.path.join(path, item)
+ for filename in os.listdir(extracted_dir):
+ shutil.move(os.path.join(extracted_dir, filename), path)
+ shutil.rmtree(extracted_dir)
+ click.secho(
+ f"✅ Downloaded and extracted repository to {path}", fg="green"
+ )
+ except error.HTTPError as e:
+ click.secho(
+ f"❌ Error: Failed to download repository.\nDetails: {e}\n",
+ fg="red",
+ bold=True,
+ err=True,
+ )
+ sys.exit(1)
+
+
+def _get_template_url(template_name: str) -> str | None:
+ """
+ Retrieves the template URL based on the provided template name.
+
+ Args:
+ template_name: The name of the template.
+
+ Returns:
+ Optional[str]: The URL of the template if found, else None.
+ """
+ if template_name in TEMPLATES:
+ click.secho(f"Template selected: {template_name}", fg="green")
+ version_choice: int = click.prompt(
+ "Choose version (1 for Python 🐍, 2 for JS/TS 🌐)", type=int
+ )
+
+ if version_choice == 1:
+ return TEMPLATES[template_name]["python"]
+ elif version_choice == 2:
+ return TEMPLATES[template_name]["js"]
+ else:
+ click.secho("❌ Invalid choice. Please try again.", fg="red")
+ return None
+ else:
+ click.secho(
+ f"Template '{template_name}' not found. Please select from the available options.",
+ fg="red",
+ )
+ return None
+
+
+def create_new(path: str | None, template: str | None) -> None:
+ """Create a new LangGraph project at the specified PATH using the chosen TEMPLATE.
+
+ Args:
+ path: The path where the new project will be created.
+ template: The name of the template to use.
+ """
+ # Prompt for path if not provided
+ if not path:
+ path = click.prompt(
+ "📂 Please specify the path to create the application", default="."
+ )
+
+ path = os.path.abspath(path) # Ensure path is absolute
+
+ # Check if path exists and is not empty
+ if os.path.exists(path) and os.listdir(path):
+ click.secho(
+ "❌ The specified directory already exists and is not empty. "
+ "Aborting to prevent overwriting files.",
+ fg="red",
+ bold=True,
+ )
+ sys.exit(1)
+
+ # Get template URL either from command-line argument or
+ # through interactive selection
+ if template:
+ if template not in TEMPLATE_ID_TO_CONFIG:
+ # Format available options in a readable way with descriptions
+ template_options = ""
+ for id_ in TEMPLATE_IDS:
+ name, lang, _ = TEMPLATE_ID_TO_CONFIG[id_]
+ description = TEMPLATES[name]["description"]
+
+ # Add each template option with color formatting
+ template_options += (
+ click.style("- ", fg="yellow", bold=True)
+ + click.style(f"{id_}", fg="cyan")
+ + click.style(f": {description}", fg="white")
+ + "\n"
+ )
+
+ # Display error message with colors and formatting
+ click.secho("❌ Error:", fg="red", bold=True, nl=False)
+ click.secho(f" Template '{template}' not found.", fg="red")
+ click.secho(
+ "Please select from the available options:\n", fg="yellow", bold=True
+ )
+ click.secho(template_options, fg="cyan")
+ sys.exit(1)
+ _, _, template_url = TEMPLATE_ID_TO_CONFIG[template]
+ else:
+ template_url = _choose_template()
+
+ # Download and extract the template
+ _download_repo_with_requests(template_url, path)
+
+ click.secho(f"🎉 New project created at {path}", fg="green", bold=True)
diff --git a/langgraph/source/libs/cli/langgraph_cli/util.py b/langgraph/source/libs/cli/langgraph_cli/util.py
new file mode 100644
index 0000000000000000000000000000000000000000..683ed44bca3a460d822b781ed035bd0a157c3fff
--- /dev/null
+++ b/langgraph/source/libs/cli/langgraph_cli/util.py
@@ -0,0 +1,25 @@
+import click
+
+
+def clean_empty_lines(input_str: str):
+ return "\n".join(filter(None, input_str.splitlines()))
+
+
+def warn_non_wolfi_distro(config_json: dict) -> None:
+ """Show warning if image_distro is not set to 'wolfi'."""
+ image_distro = config_json.get("image_distro", "debian") # Default is debian
+ if image_distro != "wolfi":
+ click.secho(
+ "⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security.",
+ fg="yellow",
+ bold=True,
+ )
+ click.secho(
+ " Wolfi is a security-oriented, minimal Linux distribution designed for containers.",
+ fg="yellow",
+ )
+ click.secho(
+ ' To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.',
+ fg="yellow",
+ )
+ click.secho("") # Empty line for better readability
diff --git a/langgraph/source/libs/cli/langgraph_cli/version.py b/langgraph/source/libs/cli/langgraph_cli/version.py
new file mode 100644
index 0000000000000000000000000000000000000000..3368893c0ea39205491fa6d63a7f23a35ffc0653
--- /dev/null
+++ b/langgraph/source/libs/cli/langgraph_cli/version.py
@@ -0,0 +1,10 @@
+"""Main entrypoint into package."""
+
+from importlib import metadata
+
+try:
+ __version__ = metadata.version(__package__)
+except metadata.PackageNotFoundError:
+ # Case where package metadata is not available.
+ __version__ = ""
+del metadata # optional, avoids polluting the results of dir(__package__)
diff --git a/langgraph/source/libs/cli/pyproject.toml b/langgraph/source/libs/cli/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..ef6d30265735148cd451cc5941e7d75668659a1f
--- /dev/null
+++ b/langgraph/source/libs/cli/pyproject.toml
@@ -0,0 +1,75 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "langgraph-cli"
+dynamic = ["version"]
+description = "CLI for interacting with LangGraph API"
+authors = []
+requires-python = ">=3.10"
+readme = "README.md"
+license = "MIT"
+license-files = ['LICENSE']
+dependencies = [
+ "click>=8.1.7",
+ "langgraph-sdk>=0.1.0 ; python_version >= '3.11'",
+]
+[tool.hatch.version]
+path = "langgraph_cli/__init__.py"
+[project.optional-dependencies]
+inmem = [
+ "langgraph-api>=0.5.35,<0.8.0 ; python_version >= '3.11'",
+ "langgraph-runtime-inmem>=0.7 ; python_version >= '3.11'",
+ "python-dotenv>=0.8.0",
+]
+
+[project.urls]
+Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/cli"
+Twitter = "https://x.com/LangChain"
+Slack = "https://www.langchain.com/join-community"
+Reddit = "https://www.reddit.com/r/LangChain/"
+
+[project.scripts]
+langgraph = "langgraph_cli.cli:cli"
+
+[dependency-groups]
+test = [
+ "pytest",
+ "pytest-asyncio",
+ "pytest-mock",
+ "pytest-watch",
+ "msgspec",
+]
+lint = [
+ "ruff",
+ "codespell",
+ "mypy",
+]
+dev = [
+ {include-group = "test"},
+ {include-group = "lint"},
+ "hatch>=1.16.2",
+]
+
+[tool.uv]
+default-groups = ['dev']
+
+[tool.hatch.build.targets.wheel]
+include = ["langgraph_cli"]
+
+[tool.pytest.ini_options]
+addopts = "--strict-markers --strict-config --durations=5 -vv"
+asyncio_mode = "auto"
+
+[tool.ruff]
+lint.select = [
+ "E", # pycodestyle
+ "F", # Pyflakes
+ "UP", # pyupgrade
+ "B", # flake8-bugbear
+ "I", # isort
+ "UP", # pyupgrade
+]
+lint.ignore = ["E501", "B008"]
+target-version = "py310"
diff --git a/langgraph/source/libs/cli/python-monorepo-example/apps/agent/.env.example b/langgraph/source/libs/cli/python-monorepo-example/apps/agent/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/python-monorepo-example/apps/agent/langgraph.json b/langgraph/source/libs/cli/python-monorepo-example/apps/agent/langgraph.json
new file mode 100644
index 0000000000000000000000000000000000000000..10cf04d1d8da72c920137bb45ae27d64121ffa98
--- /dev/null
+++ b/langgraph/source/libs/cli/python-monorepo-example/apps/agent/langgraph.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://langgra.ph/schema.json",
+ "dependencies": [".", "../../libs/shared", "../../libs/common"],
+ "graphs": {
+ "agent": "./src/agent/graph.py:graph"
+ },
+ "env": ".env"
+}
\ No newline at end of file
diff --git a/langgraph/source/libs/cli/python-monorepo-example/apps/agent/pyproject.toml b/langgraph/source/libs/cli/python-monorepo-example/apps/agent/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..43475d21a5b3d98c8ba1a42642a2b32fc844a468
--- /dev/null
+++ b/langgraph/source/libs/cli/python-monorepo-example/apps/agent/pyproject.toml
@@ -0,0 +1,19 @@
+[project]
+name = "agent"
+version = "0.0.1"
+description = "Agent for the Python monorepo"
+authors = [
+ { name = "Developer", email = "dev@example.com" },
+]
+license = { text = "MIT" }
+requires-python = ">=3.11,<4.0"
+
+[build-system]
+requires = ["setuptools>=73.0.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools]
+packages = ["agent"]
+
+[tool.setuptools.package-dir]
+"agent" = "src/agent"
\ No newline at end of file
diff --git a/langgraph/source/libs/cli/python-monorepo-example/apps/agent/src/agent/__init__.py b/langgraph/source/libs/cli/python-monorepo-example/apps/agent/src/agent/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..62617ff1c8c9768723e3da6222e0b76675e6c566
--- /dev/null
+++ b/langgraph/source/libs/cli/python-monorepo-example/apps/agent/src/agent/__init__.py
@@ -0,0 +1 @@
+"""Agent package."""
diff --git a/langgraph/source/libs/cli/python-monorepo-example/apps/agent/src/agent/graph.py b/langgraph/source/libs/cli/python-monorepo-example/apps/agent/src/agent/graph.py
new file mode 100644
index 0000000000000000000000000000000000000000..39ee31c758966665024ed1e9bcfba8e01a4a290c
--- /dev/null
+++ b/langgraph/source/libs/cli/python-monorepo-example/apps/agent/src/agent/graph.py
@@ -0,0 +1,40 @@
+"""Simple LangGraph agent for monorepo testing."""
+
+from common import get_common_prefix
+from langchain_core.messages import AIMessage
+from langgraph.graph import END, START, StateGraph
+from shared import get_dummy_message
+
+from agent.state import State
+
+
+def call_model(state: State) -> dict:
+ """Simple node that uses the shared libraries."""
+ # Use functions from both shared packages
+ dummy_message = get_dummy_message()
+ prefix = get_common_prefix()
+
+ message = AIMessage(content=f"{prefix} Agent says: {dummy_message}")
+
+ return {"messages": [message]}
+
+
+def should_continue(state: State):
+ """Conditional edge - end after first message."""
+ messages = state["messages"]
+ if len(messages) > 0:
+ return END
+ return "call_model"
+
+
+# Build the graph
+workflow = StateGraph(State)
+
+# Add the node
+workflow.add_node("call_model", call_model)
+
+# Add edges
+workflow.add_edge(START, "call_model")
+workflow.add_conditional_edges("call_model", should_continue)
+
+graph = workflow.compile()
diff --git a/langgraph/source/libs/cli/python-monorepo-example/apps/agent/src/agent/state.py b/langgraph/source/libs/cli/python-monorepo-example/apps/agent/src/agent/state.py
new file mode 100644
index 0000000000000000000000000000000000000000..8ef10fa21b2b77ea857b6e1e991e82f55a83e1ed
--- /dev/null
+++ b/langgraph/source/libs/cli/python-monorepo-example/apps/agent/src/agent/state.py
@@ -0,0 +1,13 @@
+"""State definition for the agent."""
+
+from collections.abc import Sequence
+from typing import Annotated, TypedDict
+
+from langchain_core.messages import BaseMessage
+from langgraph.graph.message import add_messages
+
+
+class State(TypedDict):
+ """The state of the agent."""
+
+ messages: Annotated[Sequence[BaseMessage], add_messages]
diff --git a/langgraph/source/libs/cli/python-monorepo-example/libs/common/__init__.py b/langgraph/source/libs/cli/python-monorepo-example/libs/common/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c49dd1ad41c18070f47bf2ebf1c85462bf9d6ce
--- /dev/null
+++ b/langgraph/source/libs/cli/python-monorepo-example/libs/common/__init__.py
@@ -0,0 +1,5 @@
+"""Common helper functions package."""
+
+from .helpers import get_common_prefix
+
+__all__ = ["get_common_prefix"]
diff --git a/langgraph/source/libs/cli/python-monorepo-example/libs/common/helpers.py b/langgraph/source/libs/cli/python-monorepo-example/libs/common/helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..f745dbddce0b0a9edad086112ff7433e299e3039
--- /dev/null
+++ b/langgraph/source/libs/cli/python-monorepo-example/libs/common/helpers.py
@@ -0,0 +1,6 @@
+"""Common helper functions."""
+
+
+def get_common_prefix() -> str:
+ """Get a common prefix for messages."""
+ return "[COMMON]"
diff --git a/langgraph/source/libs/cli/python-monorepo-example/libs/shared/pyproject.toml b/langgraph/source/libs/cli/python-monorepo-example/libs/shared/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..319411a4c2eae1a6facc7d6c035319f55f6786a5
--- /dev/null
+++ b/langgraph/source/libs/cli/python-monorepo-example/libs/shared/pyproject.toml
@@ -0,0 +1,20 @@
+[project]
+name = "shared"
+version = "0.0.1"
+description = "Shared utilities for the Python monorepo"
+authors = [
+ { name = "Developer", email = "dev@example.com" },
+]
+license = { text = "MIT" }
+requires-python = ">=3.11,<4.0"
+dependencies = []
+
+[build-system]
+requires = ["setuptools>=73.0.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools]
+packages = ["shared"]
+
+[tool.setuptools.package-dir]
+"shared" = "src/shared"
\ No newline at end of file
diff --git a/langgraph/source/libs/cli/python-monorepo-example/libs/shared/src/shared/__init__.py b/langgraph/source/libs/cli/python-monorepo-example/libs/shared/src/shared/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c30c138c1d204df8cbc7e6e35c8f49a75520f85a
--- /dev/null
+++ b/langgraph/source/libs/cli/python-monorepo-example/libs/shared/src/shared/__init__.py
@@ -0,0 +1,5 @@
+"""Shared utilities package."""
+
+from .utils import get_dummy_message
+
+__all__ = ["get_dummy_message"]
diff --git a/langgraph/source/libs/cli/python-monorepo-example/libs/shared/src/shared/utils.py b/langgraph/source/libs/cli/python-monorepo-example/libs/shared/src/shared/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..b26dcf1873f0e27fa235adb3ddbb9424ae3644b5
--- /dev/null
+++ b/langgraph/source/libs/cli/python-monorepo-example/libs/shared/src/shared/utils.py
@@ -0,0 +1,6 @@
+"""Shared utility functions."""
+
+
+def get_dummy_message() -> str:
+ """Get a dummy message for testing."""
+ return "Hello from shared library!"
diff --git a/langgraph/source/libs/cli/python-monorepo-example/pyproject.toml b/langgraph/source/libs/cli/python-monorepo-example/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..13fc4c8c508f52978949adb858c8e808de25379a
--- /dev/null
+++ b/langgraph/source/libs/cli/python-monorepo-example/pyproject.toml
@@ -0,0 +1,46 @@
+[project]
+name = "python-monorepo-example"
+version = "0.0.1"
+description = "A Python monorepo example with LangGraph agents and shared packages"
+authors = [
+ { name = "Developer", email = "dev@example.com" },
+]
+license = { text = "MIT" }
+requires-python = ">=3.11,<4.0"
+dependencies = [
+ "langgraph>=0.6.0,<2",
+ "langchain-core>=0.2.14",
+]
+
+[tool.uv.workspace]
+members = ["apps/*", "libs/shared"]
+
+[tool.uv.sources]
+shared = { workspace = true }
+
+[project.optional-dependencies]
+dev = ["mypy>=1.11.1", "ruff>=0.6.1"]
+
+[build-system]
+requires = ["setuptools>=73.0.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.ruff]
+lint.select = [
+ "E", # pycodestyle
+ "F", # pyflakes
+ "I", # isort
+ "D", # pydocstyle
+ "UP",
+]
+lint.ignore = [
+ "D100", # Missing docstring in public module
+ "D101", # Missing docstring in public class
+ "D102", # Missing docstring in public method
+ "D103", # Missing docstring in public function
+ "D104", # Missing docstring in public package
+ "D105", # Missing docstring in magic method
+]
+
+[tool.ruff.lint.pydocstyle]
+convention = "google"
\ No newline at end of file
diff --git a/langgraph/source/libs/cli/schemas/schema.json b/langgraph/source/libs/cli/schemas/schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..e4f31bb4d86373c911fd9b211c5cc04989a96098
--- /dev/null
+++ b/langgraph/source/libs/cli/schemas/schema.json
@@ -0,0 +1,1003 @@
+{
+ "$ref": "#/$defs/Config",
+ "$defs": {
+ "Config": {
+ "title": "Config",
+ "description": "Top-level config for langgraph-cli or similar deployment tooling.",
+ "type": "object",
+ "required": [],
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "python_version": {
+ "type": "string",
+ "description": "Optional. Python version in 'major.minor' format (e.g. '3.11').\nMust be at least 3.11 or greater for this deployment to function properly.\n",
+ "enum": [
+ "3.11",
+ "3.12",
+ "3.13"
+ ]
+ },
+ "pip_config_file": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, credentials, etc.).\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n"
+ },
+ "_INTERNAL_docker_tag": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Internal use only.\n"
+ },
+ "api_version": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Which semantic version of the LangGraph API server to use.\n\nDefaults to latest. Check the\nfor more information.\n"
+ },
+ "auth": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/AuthConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Custom authentication config, including the path to your Python auth logic and\nthe OpenAPI security definitions it uses.\n"
+ },
+ "base_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Base image to use for the LangGraph API server.\n\nDefaults to langchain/langgraph-api or langchain/langgraphjs-api.\n"
+ },
+ "checkpointer": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/CheckpointerConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Configuration for the built-in checkpointer, which handles checkpointing of state.\n\nIf omitted, no checkpointer is set up (the object store will still be present, however).\n"
+ },
+ "dependencies": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "List of Python dependencies to install, either from PyPI or local paths.\n"
+ },
+ "dockerfile_lines": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
+ },
+ "encryption": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/EncryptionConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Custom at-rest encryption config, including the path to your Python encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database.\n"
+ },
+ "env": {
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "description": "Optional. Environment variables to set for your deployment.\n\n- If given as a dict, keys are variable names and values are their values.\n- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.\n\nenv=\".env\n"
+ },
+ "graphs": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ },
+ "description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
+ },
+ "http": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/HttpConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed\nand how cross-origin requests are handled.\n"
+ },
+ "image_distro": {
+ "anyOf": [
+ {
+ "enum": [
+ "bookworm",
+ "bullseye",
+ "debian",
+ "wolfi"
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Linux distribution for the base image.\n\nMust be one of 'wolfi', 'debian', 'bullseye', or 'bookworm'.\nIf omitted, defaults to 'debian' ('latest').\n"
+ },
+ "keep_pkg_tools": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Control whether to retain Python packaging tools in the final image.\n\nYou can also set to true to include all packaging tools.\n"
+ },
+ "pip_installer": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Python package installer to use ('auto', 'pip', 'uv').\n\n"
+ },
+ "store": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/StoreConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
+ },
+ "ui": {
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
+ },
+ "webhooks": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/WebhooksConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
+ }
+ },
+ "required": [
+ "dependencies",
+ "graphs"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "node_version": {
+ "anyOf": [
+ {
+ "type": "string",
+ "enum": [
+ "20"
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Node.js version as a major version (e.g. '20'), if your deployment needs Node.\nMust be >= 20 if provided.\n"
+ },
+ "_INTERNAL_docker_tag": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Internal use only.\n"
+ },
+ "api_version": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Which semantic version of the LangGraph API server to use.\n\nDefaults to latest. Check the\nfor more information.\n"
+ },
+ "auth": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/AuthConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Custom authentication config, including the path to your Python auth logic and\nthe OpenAPI security definitions it uses.\n"
+ },
+ "base_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Base image to use for the LangGraph API server.\n\nDefaults to langchain/langgraph-api or langchain/langgraphjs-api.\n"
+ },
+ "checkpointer": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/CheckpointerConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Configuration for the built-in checkpointer, which handles checkpointing of state.\n\nIf omitted, no checkpointer is set up (the object store will still be present, however).\n"
+ },
+ "dependencies": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "List of Python dependencies to install, either from PyPI or local paths.\n"
+ },
+ "dockerfile_lines": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
+ },
+ "encryption": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/EncryptionConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Custom at-rest encryption config, including the path to your Python encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database.\n"
+ },
+ "env": {
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "description": "Optional. Environment variables to set for your deployment.\n\n- If given as a dict, keys are variable names and values are their values.\n- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.\n\nenv=\".env\n"
+ },
+ "graphs": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ },
+ "description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
+ },
+ "http": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/HttpConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed\nand how cross-origin requests are handled.\n"
+ },
+ "image_distro": {
+ "anyOf": [
+ {
+ "type": "string",
+ "enum": [
+ "debian",
+ "wolfi"
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Linux distribution for the base image.\n\nMust be one of 'wolfi', 'debian', 'bullseye', or 'bookworm'.\nIf omitted, defaults to 'debian' ('latest').\n"
+ },
+ "keep_pkg_tools": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Control whether to retain Python packaging tools in the final image.\n\nYou can also set to true to include all packaging tools.\n"
+ },
+ "pip_installer": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Python package installer to use ('auto', 'pip', 'uv').\n\n"
+ },
+ "store": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/StoreConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
+ },
+ "ui": {
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
+ },
+ "webhooks": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/WebhooksConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
+ }
+ },
+ "required": [
+ "node_version",
+ "graphs"
+ ]
+ }
+ ]
+ },
+ "AuthConfig": {
+ "title": "AuthConfig",
+ "description": "Configuration for custom authentication logic and how it integrates into the OpenAPI spec.",
+ "type": "object",
+ "properties": {
+ "cache": {
+ "$ref": "#/$defs/CacheConfig",
+ "description": "Optional. Cache configuration for the server.\n"
+ },
+ "disable_studio_auth": {
+ "type": "boolean",
+ "description": "Optional. Whether to disable LangSmith API-key authentication for requests originating the Studio.\n\nDefaults to False, meaning that if a particular header is set, the server will verify the `x-api-key` header\nvalue is a valid API key for the deployment's workspace. If `True`, all requests will go through your custom\nauthentication logic, regardless of origin of the request.\n"
+ },
+ "openapi": {
+ "$ref": "#/$defs/SecurityConfig",
+ "description": "The security configuration to include in your server's OpenAPI spec.\n\n{\n}\n}\n}\n},\n]\n}\n"
+ },
+ "path": {
+ "type": "string",
+ "description": "Required. Path to an instance of the Auth() class that implements custom authentication.\n"
+ }
+ },
+ "required": []
+ },
+ "CacheConfig": {
+ "title": "CacheConfig",
+ "type": "object",
+ "properties": {
+ "cache_keys": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional. List of header keys to use for caching.\n"
+ },
+ "max_size": {
+ "type": "integer",
+ "description": "Optional. Maximum size of the cache.\n"
+ },
+ "ttl_seconds": {
+ "type": "integer",
+ "description": "Optional. Time-to-live in seconds for cached items.\n"
+ }
+ },
+ "required": [],
+ "description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
+ },
+ "SecurityConfig": {
+ "title": "SecurityConfig",
+ "description": "Configuration for OpenAPI security definitions and requirements.\n\nUseful for specifying global or path-level authentication and authorization flows\n(e.g., OAuth2, API key headers, etc.).",
+ "type": "object",
+ "properties": {
+ "paths": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "description": "Path-specific security overrides.\n\n- Keys that are HTTP methods (e.g., \"GET\", \"POST\"),\n- Values are lists of security definitions (just like `security`) for that method.\n"
+ },
+ "security": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "description": "Global security requirements across all endpoints.\n\nEach element in the list maps a security scheme (e.g. \"OAuth2\") to a list of scopes (e.g. [\"read\", \"write\"])."
+ },
+ "securitySchemes": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object"
+ },
+ "description": "Describe each security scheme recognized by your OpenAPI spec.\n\nKeys are scheme names (e.g. \"OAuth2\", \"ApiKeyAuth\") and values are their definitions."
+ }
+ },
+ "required": []
+ },
+ "CheckpointerConfig": {
+ "title": "CheckpointerConfig",
+ "description": "Configuration for the built-in checkpointer, which handles checkpointing of state.\n\nIf omitted, no checkpointer is set up (the object store will still be present, however).",
+ "type": "object",
+ "properties": {
+ "serde": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/SerdeConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines the serde configuration.\n\nIf provided, the checkpointer will apply serde settings according to the configuration.\nIf omitted, no serde behavior is configured.\n\nThis configuration requires server version 0.5 or later to take effect.\n"
+ },
+ "sweep_limit": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Maximum number of threads to process per sweep iteration. Defaults to 1000."
+ },
+ "ttl": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/ThreadTTLConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines the TTL (time-to-live) behavior configuration.\n\nIf provided, the checkpointer will apply TTL settings according to the configuration.\nIf omitted, no TTL behavior is configured.\n"
+ }
+ },
+ "required": []
+ },
+ "SerdeConfig": {
+ "title": "SerdeConfig",
+ "description": "Configuration for the built-in serde, which handles checkpointing of state.\n\nIf omitted, no serde is set up (the object store will still be present, however).",
+ "type": "object",
+ "properties": {
+ "allowed_json_modules": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. List of allowed python modules to de-serialize custom objects from.\n\nIf provided, only the specified modules will be allowed to be deserialized.\nIf omitted, no modules are allowed, and the object returned will simply be a json object OR\na deserialized langchain object.\n"
+ },
+ "pickle_fallback": {
+ "type": "boolean",
+ "description": "Optional. Whether to allow pickling as a fallback for deserialization.\n\nIf True, pickling will be allowed as a fallback for deserialization.\nIf False, pickling will not be allowed as a fallback for deserialization.\nDefaults to True if not configured."
+ }
+ },
+ "required": []
+ },
+ "ThreadTTLConfig": {
+ "title": "ThreadTTLConfig",
+ "description": "Configure a default TTL for checkpointed data within threads.",
+ "type": "object",
+ "properties": {
+ "default_ttl": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Default TTL (time-to-live) in minutes for checkpointed data."
+ },
+ "strategy": {
+ "enum": [
+ "delete"
+ ],
+ "description": "Strategy to use for deleting checkpointed data.\n"
+ },
+ "sweep_interval_minutes": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Interval in minutes between sweep iterations.\nIf omitted, a default interval will be used (typically ~ 5 minutes)."
+ }
+ },
+ "required": []
+ },
+ "EncryptionConfig": {
+ "title": "EncryptionConfig",
+ "description": "Configuration for custom at-rest encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database,\nincluding metadata fields and checkpoint blobs.",
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string"
+ }
+ },
+ "required": []
+ },
+ "HttpConfig": {
+ "title": "HttpConfig",
+ "description": "Configuration for the built-in HTTP server that powers your deployment's routes and endpoints.",
+ "type": "object",
+ "properties": {
+ "app": {
+ "type": "string",
+ "description": "Optional. Import path to a custom Starlette/FastAPI application to mount.\n"
+ },
+ "configurable_headers": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/ConfigurableHeaderConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines how headers are treated for a run's configuration.\n\nYou can include or exclude headers as configurable values to condition your\nagent's behavior or permissions on a request's headers."
+ },
+ "cors": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/CorsConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines CORS restrictions. If omitted, no special rules are set and\ncross-origin behavior depends on default server settings.\n"
+ },
+ "disable_a2a": {
+ "type": "boolean",
+ "description": "Optional. If `True`, /a2a routes are removed, disabling default support to expose the deployment as an agent-to-agent (A2A) server.\n\nDefault is False.\n"
+ },
+ "disable_assistants": {
+ "type": "boolean",
+ "description": "Optional. If `True`, /assistants routes are removed from the server.\n\nDefault is False (meaning /assistants is enabled).\n"
+ },
+ "disable_mcp": {
+ "type": "boolean",
+ "description": "Optional. If `True`, /mcp routes are removed, disabling default support to expose the deployment as an MCP server.\n\nDefault is False.\n"
+ },
+ "disable_meta": {
+ "type": "boolean",
+ "description": "Optional. Remove meta endpoints.\n\n\nDefault is False.\n"
+ },
+ "disable_runs": {
+ "type": "boolean",
+ "description": "Optional. If `True`, /runs routes are removed.\n\nDefault is False.\n"
+ },
+ "disable_store": {
+ "type": "boolean",
+ "description": "Optional. If `True`, /store routes are removed, disabling direct store interactions via HTTP.\n\nDefault is False.\n"
+ },
+ "disable_threads": {
+ "type": "boolean",
+ "description": "Optional. If `True`, /threads routes are removed.\n\nDefault is False.\n"
+ },
+ "disable_ui": {
+ "type": "boolean",
+ "description": "Optional. If `True`, /ui routes are removed, disabling the UI server.\n\nDefault is False.\n"
+ },
+ "disable_webhooks": {
+ "type": "boolean",
+ "description": "Optional. If `True`, webhooks are disabled. Runs created with an associated webhook will\nstill be executed, but the webhook event will not be sent.\n\nDefault is False.\n"
+ },
+ "enable_custom_route_auth": {
+ "type": "boolean",
+ "description": "Optional. If `True`, authentication is enabled for custom routes,\nnot just the routes that are protected by default.\n(Routes protected by default include /assistants, /threads, and /runs).\n\nDefault is False. This flag only affects authentication behavior\nif `app` is provided and contains custom routes.\n"
+ },
+ "logging_headers": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/ConfigurableHeaderConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines which headers are excluded from logging."
+ },
+ "middleware_order": {
+ "anyOf": [
+ {
+ "enum": [
+ "auth_first",
+ "middleware_first"
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines the order in which to apply server customizations.\n"
+ },
+ "mount_prefix": {
+ "type": "string",
+ "description": "Optional. URL prefix to prepend to all the routes.\n"
+ }
+ },
+ "required": []
+ },
+ "ConfigurableHeaderConfig": {
+ "title": "ConfigurableHeaderConfig",
+ "description": "Customize which headers to include as configurable values in your runs.\n\nBy default, omits x-api-key, x-tenant-id, and x-service-key.\n\nExclusions (if provided) take precedence.\n\nEach value can be a raw string with an optional wildcard.",
+ "type": "object",
+ "properties": {
+ "excludes": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Headers to exclude. Applied before the 'includes' checks.\n"
+ },
+ "includes": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Headers to include (if not also matched against an 'excludes' pattern).\n"
+ }
+ },
+ "required": []
+ },
+ "CorsConfig": {
+ "title": "CorsConfig",
+ "description": "Specifies Cross-Origin Resource Sharing (CORS) rules for your server.\n\nIf omitted, defaults are typically very restrictive (often no cross-origin requests).\nConfigure carefully if you want to allow usage from browsers hosted on other domains.",
+ "type": "object",
+ "properties": {
+ "allow_credentials": {
+ "type": "boolean",
+ "description": "Optional. If `True`, cross-origin requests can include credentials (cookies, auth headers).\n\nDefault False to avoid accidentally exposing secured endpoints to untrusted sites.\n"
+ },
+ "allow_headers": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional. HTTP headers that can be used in cross-origin requests (e.g. [\"Content-Type\", \"Authorization\"])."
+ },
+ "allow_methods": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional. HTTP methods permitted for cross-origin requests (e.g. [\"GET\", \"POST\"]).\n\nDefault might be [\"GET\", \"POST\", \"OPTIONS\"] depending on your server framework.\n"
+ },
+ "allow_origin_regex": {
+ "type": "string",
+ "description": "Optional. A regex pattern for matching allowed origins, used if you have dynamic subdomains.\n"
+ },
+ "allow_origins": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional. List of allowed origins (e.g., \"https://example.com\").\n\nDefault is often an empty list (no external origins).\nUse \"*\" only if you trust all origins, as that bypasses most restrictions.\n"
+ },
+ "expose_headers": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional. List of headers that browsers are allowed to read from the response in cross-origin contexts."
+ },
+ "max_age": {
+ "type": "integer",
+ "description": "Optional. How many seconds the browser may cache preflight responses.\n\nDefault might be 600 (10 minutes). Larger values reduce preflight requests but can cause stale configurations.\n"
+ }
+ },
+ "required": []
+ },
+ "StoreConfig": {
+ "title": "StoreConfig",
+ "description": "Configuration for the built-in long-term memory store.\n\nThis store can optionally perform semantic search. If you omit `index`,\nthe store will just handle traditional (non-embedded) data without vector lookups.",
+ "type": "object",
+ "properties": {
+ "index": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/IndexConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines the vector-based semantic search configuration.\n\n- Generate embeddings according to `index.embed`\n- Enforce the embedding dimension given by `index.dims`\n- Embed only specified JSON fields (if any) from `index.fields`\n\nIf omitted, no vector index is initialized.\n"
+ },
+ "ttl": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/TTLConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines the TTL (time-to-live) behavior configuration.\n\nIf provided, the store will apply TTL settings according to the configuration.\nIf omitted, no TTL behavior is configured.\n"
+ }
+ },
+ "required": []
+ },
+ "IndexConfig": {
+ "title": "IndexConfig",
+ "description": "Configuration for indexing documents for semantic search in the store.\n\nThis governs how text is converted into embeddings and stored for vector-based lookups.",
+ "type": "object",
+ "properties": {
+ "dims": {
+ "type": "integer",
+ "description": "Required. Dimensionality of the embedding vectors you will store.\n\nMust match the output dimension of your selected embedding model or custom embed function.\nIf mismatched, you will likely encounter shape/size errors when inserting or querying vectors.\n\n"
+ },
+ "embed": {
+ "type": "string",
+ "description": "Required. Identifier or reference to the embedding model or a custom embedding function.\n\n- \"my_custom_embed\" if it's a known alias in your system\n"
+ },
+ "fields": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. List of JSON fields to extract before generating embeddings.\n\nDefaults to [\"$\"], which means the entire JSON object is embedded as one piece of text.\nIf you provide multiple fields (e.g. [\"title\", \"content\"]), each is extracted and embedded separately,\noften saving token usage if you only care about certain parts of the data.\n"
+ }
+ },
+ "required": []
+ },
+ "TTLConfig": {
+ "title": "TTLConfig",
+ "description": "Configuration for TTL (time-to-live) behavior in the store.",
+ "type": "object",
+ "properties": {
+ "default_ttl": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Default TTL (time-to-live) in minutes for new items.\n\nIf provided, all new items will have this TTL unless explicitly overridden.\nIf omitted, items will have no TTL by default.\n"
+ },
+ "refresh_on_read": {
+ "type": "boolean",
+ "description": "Default behavior for refreshing TTLs on read operations (`GET` and `SEARCH`).\n\nIf `True`, TTLs will be refreshed on read operations (get/search) by default.\nThis can be overridden per-operation by explicitly setting `refresh_ttl`.\nDefaults to `True` if not configured.\n"
+ },
+ "sweep_interval_minutes": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Interval in minutes between TTL sweep iterations.\n\nIf provided, the store will periodically delete expired items based on the TTL.\nIf omitted, no automatic sweeping will occur.\n"
+ }
+ },
+ "required": []
+ },
+ "WebhooksConfig": {
+ "title": "WebhooksConfig",
+ "type": "object",
+ "properties": {
+ "env_prefix": {
+ "type": "string",
+ "description": "Required prefix for environment variables referenced in header templates.\n\nActs as an allowlist boundary to prevent leaking arbitrary environment\nvariables. Defaults to \"LG_WEBHOOK_\" when omitted.\n"
+ },
+ "headers": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ },
+ "description": "Static headers to include with webhook requests.\n\nValues may contain templates of the form \"${{ env.VAR }}\". On startup, these\nare resolved via the process environment after verifying `VAR` starts with\n`env_prefix`. Mixed literals and multiple templates are allowed.\n"
+ },
+ "url": {
+ "$ref": "#/$defs/WebhookUrlPolicy",
+ "description": "URL validation policy for user-supplied webhook endpoints."
+ }
+ },
+ "required": [],
+ "description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
+ },
+ "WebhookUrlPolicy": {
+ "title": "WebhookUrlPolicy",
+ "type": "object",
+ "properties": {
+ "allowed_domains": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Hostname allowlist. Supports exact hosts and wildcard subdomains.\n\nUse entries like \"hooks.example.com\" or \"*.mycorp.com\". The wildcard only\nmatches subdomains (\"foo.mycorp.com\"), not the apex (\"mycorp.com\"). When\nempty or omitted, any public host is allowed (subject to SSRF IP checks).\n"
+ },
+ "allowed_ports": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ },
+ "description": "Explicit port allowlist for absolute URLs.\n\nIf set, requests must use one of these ports. Defaults are respected when\na port is not present in the URL (443 for https, 80 for http).\n"
+ },
+ "disable_loopback": {
+ "type": "boolean",
+ "description": "Disallow relative URLs (internal loopback calls) when true."
+ },
+ "max_url_length": {
+ "type": "integer",
+ "description": "Maximum permitted URL length in characters; longer inputs are rejected early."
+ },
+ "require_https": {
+ "type": "boolean",
+ "description": "Enforce HTTPS scheme for absolute URLs; reject `http://` when true."
+ }
+ },
+ "required": [],
+ "description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
+ }
+ },
+ "title": "LangGraph CLI Configuration",
+ "description": "Configuration schema for langgraph-cli",
+ "version": "v0"
+}
\ No newline at end of file
diff --git a/langgraph/source/libs/cli/schemas/schema.v0.json b/langgraph/source/libs/cli/schemas/schema.v0.json
new file mode 100644
index 0000000000000000000000000000000000000000..e4f31bb4d86373c911fd9b211c5cc04989a96098
--- /dev/null
+++ b/langgraph/source/libs/cli/schemas/schema.v0.json
@@ -0,0 +1,1003 @@
+{
+ "$ref": "#/$defs/Config",
+ "$defs": {
+ "Config": {
+ "title": "Config",
+ "description": "Top-level config for langgraph-cli or similar deployment tooling.",
+ "type": "object",
+ "required": [],
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "python_version": {
+ "type": "string",
+ "description": "Optional. Python version in 'major.minor' format (e.g. '3.11').\nMust be at least 3.11 or greater for this deployment to function properly.\n",
+ "enum": [
+ "3.11",
+ "3.12",
+ "3.13"
+ ]
+ },
+ "pip_config_file": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, credentials, etc.).\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n"
+ },
+ "_INTERNAL_docker_tag": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Internal use only.\n"
+ },
+ "api_version": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Which semantic version of the LangGraph API server to use.\n\nDefaults to latest. Check the\nfor more information.\n"
+ },
+ "auth": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/AuthConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Custom authentication config, including the path to your Python auth logic and\nthe OpenAPI security definitions it uses.\n"
+ },
+ "base_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Base image to use for the LangGraph API server.\n\nDefaults to langchain/langgraph-api or langchain/langgraphjs-api.\n"
+ },
+ "checkpointer": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/CheckpointerConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Configuration for the built-in checkpointer, which handles checkpointing of state.\n\nIf omitted, no checkpointer is set up (the object store will still be present, however).\n"
+ },
+ "dependencies": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "List of Python dependencies to install, either from PyPI or local paths.\n"
+ },
+ "dockerfile_lines": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
+ },
+ "encryption": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/EncryptionConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Custom at-rest encryption config, including the path to your Python encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database.\n"
+ },
+ "env": {
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "description": "Optional. Environment variables to set for your deployment.\n\n- If given as a dict, keys are variable names and values are their values.\n- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.\n\nenv=\".env\n"
+ },
+ "graphs": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ },
+ "description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
+ },
+ "http": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/HttpConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed\nand how cross-origin requests are handled.\n"
+ },
+ "image_distro": {
+ "anyOf": [
+ {
+ "enum": [
+ "bookworm",
+ "bullseye",
+ "debian",
+ "wolfi"
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Linux distribution for the base image.\n\nMust be one of 'wolfi', 'debian', 'bullseye', or 'bookworm'.\nIf omitted, defaults to 'debian' ('latest').\n"
+ },
+ "keep_pkg_tools": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Control whether to retain Python packaging tools in the final image.\n\nYou can also set to true to include all packaging tools.\n"
+ },
+ "pip_installer": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Python package installer to use ('auto', 'pip', 'uv').\n\n"
+ },
+ "store": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/StoreConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
+ },
+ "ui": {
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
+ },
+ "webhooks": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/WebhooksConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
+ }
+ },
+ "required": [
+ "dependencies",
+ "graphs"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "node_version": {
+ "anyOf": [
+ {
+ "type": "string",
+ "enum": [
+ "20"
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Node.js version as a major version (e.g. '20'), if your deployment needs Node.\nMust be >= 20 if provided.\n"
+ },
+ "_INTERNAL_docker_tag": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Internal use only.\n"
+ },
+ "api_version": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Which semantic version of the LangGraph API server to use.\n\nDefaults to latest. Check the\nfor more information.\n"
+ },
+ "auth": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/AuthConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Custom authentication config, including the path to your Python auth logic and\nthe OpenAPI security definitions it uses.\n"
+ },
+ "base_image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Base image to use for the LangGraph API server.\n\nDefaults to langchain/langgraph-api or langchain/langgraphjs-api.\n"
+ },
+ "checkpointer": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/CheckpointerConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Configuration for the built-in checkpointer, which handles checkpointing of state.\n\nIf omitted, no checkpointer is set up (the object store will still be present, however).\n"
+ },
+ "dependencies": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "List of Python dependencies to install, either from PyPI or local paths.\n"
+ },
+ "dockerfile_lines": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
+ },
+ "encryption": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/EncryptionConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Custom at-rest encryption config, including the path to your Python encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database.\n"
+ },
+ "env": {
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "description": "Optional. Environment variables to set for your deployment.\n\n- If given as a dict, keys are variable names and values are their values.\n- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.\n\nenv=\".env\n"
+ },
+ "graphs": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ },
+ "description": "Optional. Named definitions of graphs, each pointing to a Python object.\n\n\nGraphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context\nmanagers that accept a single configuration argument (of type RunnableConfig) and return a pregel object\n(instance of Stategraph, etc.).\n"
+ },
+ "http": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/HttpConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed\nand how cross-origin requests are handled.\n"
+ },
+ "image_distro": {
+ "anyOf": [
+ {
+ "type": "string",
+ "enum": [
+ "debian",
+ "wolfi"
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Linux distribution for the base image.\n\nMust be one of 'wolfi', 'debian', 'bullseye', or 'bookworm'.\nIf omitted, defaults to 'debian' ('latest').\n"
+ },
+ "keep_pkg_tools": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Control whether to retain Python packaging tools in the final image.\n\nYou can also set to true to include all packaging tools.\n"
+ },
+ "pip_installer": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Python package installer to use ('auto', 'pip', 'uv').\n\n"
+ },
+ "store": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/StoreConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
+ },
+ "ui": {
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
+ },
+ "webhooks": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/WebhooksConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
+ }
+ },
+ "required": [
+ "node_version",
+ "graphs"
+ ]
+ }
+ ]
+ },
+ "AuthConfig": {
+ "title": "AuthConfig",
+ "description": "Configuration for custom authentication logic and how it integrates into the OpenAPI spec.",
+ "type": "object",
+ "properties": {
+ "cache": {
+ "$ref": "#/$defs/CacheConfig",
+ "description": "Optional. Cache configuration for the server.\n"
+ },
+ "disable_studio_auth": {
+ "type": "boolean",
+ "description": "Optional. Whether to disable LangSmith API-key authentication for requests originating the Studio.\n\nDefaults to False, meaning that if a particular header is set, the server will verify the `x-api-key` header\nvalue is a valid API key for the deployment's workspace. If `True`, all requests will go through your custom\nauthentication logic, regardless of origin of the request.\n"
+ },
+ "openapi": {
+ "$ref": "#/$defs/SecurityConfig",
+ "description": "The security configuration to include in your server's OpenAPI spec.\n\n{\n}\n}\n}\n},\n]\n}\n"
+ },
+ "path": {
+ "type": "string",
+ "description": "Required. Path to an instance of the Auth() class that implements custom authentication.\n"
+ }
+ },
+ "required": []
+ },
+ "CacheConfig": {
+ "title": "CacheConfig",
+ "type": "object",
+ "properties": {
+ "cache_keys": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional. List of header keys to use for caching.\n"
+ },
+ "max_size": {
+ "type": "integer",
+ "description": "Optional. Maximum size of the cache.\n"
+ },
+ "ttl_seconds": {
+ "type": "integer",
+ "description": "Optional. Time-to-live in seconds for cached items.\n"
+ }
+ },
+ "required": [],
+ "description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
+ },
+ "SecurityConfig": {
+ "title": "SecurityConfig",
+ "description": "Configuration for OpenAPI security definitions and requirements.\n\nUseful for specifying global or path-level authentication and authorization flows\n(e.g., OAuth2, API key headers, etc.).",
+ "type": "object",
+ "properties": {
+ "paths": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "description": "Path-specific security overrides.\n\n- Keys that are HTTP methods (e.g., \"GET\", \"POST\"),\n- Values are lists of security definitions (just like `security`) for that method.\n"
+ },
+ "security": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "description": "Global security requirements across all endpoints.\n\nEach element in the list maps a security scheme (e.g. \"OAuth2\") to a list of scopes (e.g. [\"read\", \"write\"])."
+ },
+ "securitySchemes": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object"
+ },
+ "description": "Describe each security scheme recognized by your OpenAPI spec.\n\nKeys are scheme names (e.g. \"OAuth2\", \"ApiKeyAuth\") and values are their definitions."
+ }
+ },
+ "required": []
+ },
+ "CheckpointerConfig": {
+ "title": "CheckpointerConfig",
+ "description": "Configuration for the built-in checkpointer, which handles checkpointing of state.\n\nIf omitted, no checkpointer is set up (the object store will still be present, however).",
+ "type": "object",
+ "properties": {
+ "serde": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/SerdeConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines the serde configuration.\n\nIf provided, the checkpointer will apply serde settings according to the configuration.\nIf omitted, no serde behavior is configured.\n\nThis configuration requires server version 0.5 or later to take effect.\n"
+ },
+ "sweep_limit": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Maximum number of threads to process per sweep iteration. Defaults to 1000."
+ },
+ "ttl": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/ThreadTTLConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines the TTL (time-to-live) behavior configuration.\n\nIf provided, the checkpointer will apply TTL settings according to the configuration.\nIf omitted, no TTL behavior is configured.\n"
+ }
+ },
+ "required": []
+ },
+ "SerdeConfig": {
+ "title": "SerdeConfig",
+ "description": "Configuration for the built-in serde, which handles checkpointing of state.\n\nIf omitted, no serde is set up (the object store will still be present, however).",
+ "type": "object",
+ "properties": {
+ "allowed_json_modules": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. List of allowed python modules to de-serialize custom objects from.\n\nIf provided, only the specified modules will be allowed to be deserialized.\nIf omitted, no modules are allowed, and the object returned will simply be a json object OR\na deserialized langchain object.\n"
+ },
+ "pickle_fallback": {
+ "type": "boolean",
+ "description": "Optional. Whether to allow pickling as a fallback for deserialization.\n\nIf True, pickling will be allowed as a fallback for deserialization.\nIf False, pickling will not be allowed as a fallback for deserialization.\nDefaults to True if not configured."
+ }
+ },
+ "required": []
+ },
+ "ThreadTTLConfig": {
+ "title": "ThreadTTLConfig",
+ "description": "Configure a default TTL for checkpointed data within threads.",
+ "type": "object",
+ "properties": {
+ "default_ttl": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Default TTL (time-to-live) in minutes for checkpointed data."
+ },
+ "strategy": {
+ "enum": [
+ "delete"
+ ],
+ "description": "Strategy to use for deleting checkpointed data.\n"
+ },
+ "sweep_interval_minutes": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Interval in minutes between sweep iterations.\nIf omitted, a default interval will be used (typically ~ 5 minutes)."
+ }
+ },
+ "required": []
+ },
+ "EncryptionConfig": {
+ "title": "EncryptionConfig",
+ "description": "Configuration for custom at-rest encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database,\nincluding metadata fields and checkpoint blobs.",
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string"
+ }
+ },
+ "required": []
+ },
+ "HttpConfig": {
+ "title": "HttpConfig",
+ "description": "Configuration for the built-in HTTP server that powers your deployment's routes and endpoints.",
+ "type": "object",
+ "properties": {
+ "app": {
+ "type": "string",
+ "description": "Optional. Import path to a custom Starlette/FastAPI application to mount.\n"
+ },
+ "configurable_headers": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/ConfigurableHeaderConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines how headers are treated for a run's configuration.\n\nYou can include or exclude headers as configurable values to condition your\nagent's behavior or permissions on a request's headers."
+ },
+ "cors": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/CorsConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines CORS restrictions. If omitted, no special rules are set and\ncross-origin behavior depends on default server settings.\n"
+ },
+ "disable_a2a": {
+ "type": "boolean",
+ "description": "Optional. If `True`, /a2a routes are removed, disabling default support to expose the deployment as an agent-to-agent (A2A) server.\n\nDefault is False.\n"
+ },
+ "disable_assistants": {
+ "type": "boolean",
+ "description": "Optional. If `True`, /assistants routes are removed from the server.\n\nDefault is False (meaning /assistants is enabled).\n"
+ },
+ "disable_mcp": {
+ "type": "boolean",
+ "description": "Optional. If `True`, /mcp routes are removed, disabling default support to expose the deployment as an MCP server.\n\nDefault is False.\n"
+ },
+ "disable_meta": {
+ "type": "boolean",
+ "description": "Optional. Remove meta endpoints.\n\n\nDefault is False.\n"
+ },
+ "disable_runs": {
+ "type": "boolean",
+ "description": "Optional. If `True`, /runs routes are removed.\n\nDefault is False.\n"
+ },
+ "disable_store": {
+ "type": "boolean",
+ "description": "Optional. If `True`, /store routes are removed, disabling direct store interactions via HTTP.\n\nDefault is False.\n"
+ },
+ "disable_threads": {
+ "type": "boolean",
+ "description": "Optional. If `True`, /threads routes are removed.\n\nDefault is False.\n"
+ },
+ "disable_ui": {
+ "type": "boolean",
+ "description": "Optional. If `True`, /ui routes are removed, disabling the UI server.\n\nDefault is False.\n"
+ },
+ "disable_webhooks": {
+ "type": "boolean",
+ "description": "Optional. If `True`, webhooks are disabled. Runs created with an associated webhook will\nstill be executed, but the webhook event will not be sent.\n\nDefault is False.\n"
+ },
+ "enable_custom_route_auth": {
+ "type": "boolean",
+ "description": "Optional. If `True`, authentication is enabled for custom routes,\nnot just the routes that are protected by default.\n(Routes protected by default include /assistants, /threads, and /runs).\n\nDefault is False. This flag only affects authentication behavior\nif `app` is provided and contains custom routes.\n"
+ },
+ "logging_headers": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/ConfigurableHeaderConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines which headers are excluded from logging."
+ },
+ "middleware_order": {
+ "anyOf": [
+ {
+ "enum": [
+ "auth_first",
+ "middleware_first"
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines the order in which to apply server customizations.\n"
+ },
+ "mount_prefix": {
+ "type": "string",
+ "description": "Optional. URL prefix to prepend to all the routes.\n"
+ }
+ },
+ "required": []
+ },
+ "ConfigurableHeaderConfig": {
+ "title": "ConfigurableHeaderConfig",
+ "description": "Customize which headers to include as configurable values in your runs.\n\nBy default, omits x-api-key, x-tenant-id, and x-service-key.\n\nExclusions (if provided) take precedence.\n\nEach value can be a raw string with an optional wildcard.",
+ "type": "object",
+ "properties": {
+ "excludes": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Headers to exclude. Applied before the 'includes' checks.\n"
+ },
+ "includes": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Headers to include (if not also matched against an 'excludes' pattern).\n"
+ }
+ },
+ "required": []
+ },
+ "CorsConfig": {
+ "title": "CorsConfig",
+ "description": "Specifies Cross-Origin Resource Sharing (CORS) rules for your server.\n\nIf omitted, defaults are typically very restrictive (often no cross-origin requests).\nConfigure carefully if you want to allow usage from browsers hosted on other domains.",
+ "type": "object",
+ "properties": {
+ "allow_credentials": {
+ "type": "boolean",
+ "description": "Optional. If `True`, cross-origin requests can include credentials (cookies, auth headers).\n\nDefault False to avoid accidentally exposing secured endpoints to untrusted sites.\n"
+ },
+ "allow_headers": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional. HTTP headers that can be used in cross-origin requests (e.g. [\"Content-Type\", \"Authorization\"])."
+ },
+ "allow_methods": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional. HTTP methods permitted for cross-origin requests (e.g. [\"GET\", \"POST\"]).\n\nDefault might be [\"GET\", \"POST\", \"OPTIONS\"] depending on your server framework.\n"
+ },
+ "allow_origin_regex": {
+ "type": "string",
+ "description": "Optional. A regex pattern for matching allowed origins, used if you have dynamic subdomains.\n"
+ },
+ "allow_origins": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional. List of allowed origins (e.g., \"https://example.com\").\n\nDefault is often an empty list (no external origins).\nUse \"*\" only if you trust all origins, as that bypasses most restrictions.\n"
+ },
+ "expose_headers": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Optional. List of headers that browsers are allowed to read from the response in cross-origin contexts."
+ },
+ "max_age": {
+ "type": "integer",
+ "description": "Optional. How many seconds the browser may cache preflight responses.\n\nDefault might be 600 (10 minutes). Larger values reduce preflight requests but can cause stale configurations.\n"
+ }
+ },
+ "required": []
+ },
+ "StoreConfig": {
+ "title": "StoreConfig",
+ "description": "Configuration for the built-in long-term memory store.\n\nThis store can optionally perform semantic search. If you omit `index`,\nthe store will just handle traditional (non-embedded) data without vector lookups.",
+ "type": "object",
+ "properties": {
+ "index": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/IndexConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines the vector-based semantic search configuration.\n\n- Generate embeddings according to `index.embed`\n- Enforce the embedding dimension given by `index.dims`\n- Embed only specified JSON fields (if any) from `index.fields`\n\nIf omitted, no vector index is initialized.\n"
+ },
+ "ttl": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/TTLConfig"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Defines the TTL (time-to-live) behavior configuration.\n\nIf provided, the store will apply TTL settings according to the configuration.\nIf omitted, no TTL behavior is configured.\n"
+ }
+ },
+ "required": []
+ },
+ "IndexConfig": {
+ "title": "IndexConfig",
+ "description": "Configuration for indexing documents for semantic search in the store.\n\nThis governs how text is converted into embeddings and stored for vector-based lookups.",
+ "type": "object",
+ "properties": {
+ "dims": {
+ "type": "integer",
+ "description": "Required. Dimensionality of the embedding vectors you will store.\n\nMust match the output dimension of your selected embedding model or custom embed function.\nIf mismatched, you will likely encounter shape/size errors when inserting or querying vectors.\n\n"
+ },
+ "embed": {
+ "type": "string",
+ "description": "Required. Identifier or reference to the embedding model or a custom embedding function.\n\n- \"my_custom_embed\" if it's a known alias in your system\n"
+ },
+ "fields": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. List of JSON fields to extract before generating embeddings.\n\nDefaults to [\"$\"], which means the entire JSON object is embedded as one piece of text.\nIf you provide multiple fields (e.g. [\"title\", \"content\"]), each is extracted and embedded separately,\noften saving token usage if you only care about certain parts of the data.\n"
+ }
+ },
+ "required": []
+ },
+ "TTLConfig": {
+ "title": "TTLConfig",
+ "description": "Configuration for TTL (time-to-live) behavior in the store.",
+ "type": "object",
+ "properties": {
+ "default_ttl": {
+ "anyOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Default TTL (time-to-live) in minutes for new items.\n\nIf provided, all new items will have this TTL unless explicitly overridden.\nIf omitted, items will have no TTL by default.\n"
+ },
+ "refresh_on_read": {
+ "type": "boolean",
+ "description": "Default behavior for refreshing TTLs on read operations (`GET` and `SEARCH`).\n\nIf `True`, TTLs will be refreshed on read operations (get/search) by default.\nThis can be overridden per-operation by explicitly setting `refresh_ttl`.\nDefaults to `True` if not configured.\n"
+ },
+ "sweep_interval_minutes": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional. Interval in minutes between TTL sweep iterations.\n\nIf provided, the store will periodically delete expired items based on the TTL.\nIf omitted, no automatic sweeping will occur.\n"
+ }
+ },
+ "required": []
+ },
+ "WebhooksConfig": {
+ "title": "WebhooksConfig",
+ "type": "object",
+ "properties": {
+ "env_prefix": {
+ "type": "string",
+ "description": "Required prefix for environment variables referenced in header templates.\n\nActs as an allowlist boundary to prevent leaking arbitrary environment\nvariables. Defaults to \"LG_WEBHOOK_\" when omitted.\n"
+ },
+ "headers": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ },
+ "description": "Static headers to include with webhook requests.\n\nValues may contain templates of the form \"${{ env.VAR }}\". On startup, these\nare resolved via the process environment after verifying `VAR` starts with\n`env_prefix`. Mixed literals and multiple templates are allowed.\n"
+ },
+ "url": {
+ "$ref": "#/$defs/WebhookUrlPolicy",
+ "description": "URL validation policy for user-supplied webhook endpoints."
+ }
+ },
+ "required": [],
+ "description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
+ },
+ "WebhookUrlPolicy": {
+ "title": "WebhookUrlPolicy",
+ "type": "object",
+ "properties": {
+ "allowed_domains": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Hostname allowlist. Supports exact hosts and wildcard subdomains.\n\nUse entries like \"hooks.example.com\" or \"*.mycorp.com\". The wildcard only\nmatches subdomains (\"foo.mycorp.com\"), not the apex (\"mycorp.com\"). When\nempty or omitted, any public host is allowed (subject to SSRF IP checks).\n"
+ },
+ "allowed_ports": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ },
+ "description": "Explicit port allowlist for absolute URLs.\n\nIf set, requests must use one of these ports. Defaults are respected when\na port is not present in the URL (443 for https, 80 for http).\n"
+ },
+ "disable_loopback": {
+ "type": "boolean",
+ "description": "Disallow relative URLs (internal loopback calls) when true."
+ },
+ "max_url_length": {
+ "type": "integer",
+ "description": "Maximum permitted URL length in characters; longer inputs are rejected early."
+ },
+ "require_https": {
+ "type": "boolean",
+ "description": "Enforce HTTPS scheme for absolute URLs; reject `http://` when true."
+ }
+ },
+ "required": [],
+ "description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
+ }
+ },
+ "title": "LangGraph CLI Configuration",
+ "description": "Configuration schema for langgraph-cli",
+ "version": "v0"
+}
\ No newline at end of file
diff --git a/langgraph/source/libs/cli/schemas/version.schema.json b/langgraph/source/libs/cli/schemas/version.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..041b130c177dcd4cd126840ab046ea331696b0c8
--- /dev/null
+++ b/langgraph/source/libs/cli/schemas/version.schema.json
@@ -0,0 +1,46 @@
+{
+ "$id": "https://github.com/langchain-ai/langgraph/libs/cli/schemas/version.schema.json",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "LangGraph Platform configuration (when building via the langgraph-cli).",
+ "type": "object",
+ "oneOf": [
+ {
+ "allOf": [
+ {
+ "oneOf": [
+ {
+ "properties": {
+ "version": {
+ "type": "string",
+ "maxLength": 0
+ }
+ },
+ "required": ["version"]
+ },
+ {
+ "not": {
+ "required": ["version"]
+ }
+ }
+ ]
+ },
+ {
+ "$ref": "https://raw.githubusercontent.com/langchain-ai/langgraph/main/libs/cli/schemas/schema.json"
+ }
+ ]
+ },
+ {
+ "allOf": [
+ {
+ "properties": {
+ "version": { "const": "v0" }
+ },
+ "required": ["version"]
+ },
+ {
+ "$ref": "https://raw.githubusercontent.com/langchain-ai/langgraph/main/libs/cli/schemas/schema.v0.json"
+ }
+ ]
+ }
+ ]
+}
diff --git a/langgraph/source/libs/cli/tests/__init__.py b/langgraph/source/libs/cli/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/tests/integration_tests/__init__.py b/langgraph/source/libs/cli/tests/integration_tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/tests/integration_tests/test_cli.py b/langgraph/source/libs/cli/tests/integration_tests/test_cli.py
new file mode 100644
index 0000000000000000000000000000000000000000..7cb41b47bf9e74770a99ad90cb4b7b51d560b712
--- /dev/null
+++ b/langgraph/source/libs/cli/tests/integration_tests/test_cli.py
@@ -0,0 +1,13 @@
+import pytest
+import requests
+
+from langgraph_cli.templates import TEMPLATE_ID_TO_CONFIG
+
+
+@pytest.mark.parametrize("template_key", TEMPLATE_ID_TO_CONFIG.keys())
+def test_template_urls_work(template_key: str) -> None:
+ """Integration test to verify that all template URLs are reachable."""
+ _, _, template_url = TEMPLATE_ID_TO_CONFIG[template_key]
+ response = requests.head(template_url)
+ # Returns 302 on a successful HEAD request
+ assert response.status_code == 302, f"URL {template_url} is not reachable."
diff --git a/langgraph/source/libs/cli/tests/unit_tests/__init__.py b/langgraph/source/libs/cli/tests/unit_tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/tests/unit_tests/agent.py b/langgraph/source/libs/cli/tests/unit_tests/agent.py
new file mode 100644
index 0000000000000000000000000000000000000000..214a4919897be0fdd71074ed16428ee82221398a
--- /dev/null
+++ b/langgraph/source/libs/cli/tests/unit_tests/agent.py
@@ -0,0 +1,82 @@
+import asyncio
+import os
+from collections.abc import Sequence
+from typing import Annotated, TypedDict
+
+from langchain_core.language_models.fake_chat_models import FakeListChatModel
+from langchain_core.messages import BaseMessage, HumanMessage, ToolMessage
+from langgraph.graph import END, StateGraph, add_messages
+
+# check that env var is present
+os.environ["SOME_ENV_VAR"]
+
+
+class AgentState(TypedDict):
+ some_bytes: bytes
+ some_byte_array: bytearray
+ dict_with_bytes: dict[str, bytes]
+ messages: Annotated[Sequence[BaseMessage], add_messages]
+ sleep: int
+
+
+async def call_model(state, config):
+ if sleep := state.get("sleep"):
+ await asyncio.sleep(sleep)
+
+ messages = state["messages"]
+
+ if len(messages) > 1:
+ assert state["some_bytes"] == b"some_bytes"
+ assert state["some_byte_array"] == bytearray(b"some_byte_array")
+ assert state["dict_with_bytes"] == {"more_bytes": b"more_bytes"}
+
+ # hacky way to reset model to the "first" response
+ if isinstance(messages[-1], HumanMessage):
+ model.i = 0
+
+ response = await model.ainvoke(messages)
+ return {
+ "messages": [response],
+ "some_bytes": b"some_bytes",
+ "some_byte_array": bytearray(b"some_byte_array"),
+ "dict_with_bytes": {"more_bytes": b"more_bytes"},
+ }
+
+
+def call_tool(state):
+ last_message_content = state["messages"][-1].content
+ return {
+ "messages": [
+ ToolMessage(
+ f"tool_call__{last_message_content}", tool_call_id="tool_call_id"
+ )
+ ]
+ }
+
+
+def should_continue(state):
+ messages = state["messages"]
+ last_message = messages[-1]
+ if last_message.content == "end":
+ return END
+ else:
+ return "tool"
+
+
+# NOTE: the model cycles through responses infinitely here
+model = FakeListChatModel(responses=["begin", "end"])
+workflow = StateGraph(AgentState)
+
+workflow.add_node("agent", call_model)
+workflow.add_node("tool", call_tool)
+
+workflow.set_entry_point("agent")
+
+workflow.add_conditional_edges(
+ "agent",
+ should_continue,
+)
+
+workflow.add_edge("tool", "agent")
+
+graph = workflow.compile()
diff --git a/langgraph/source/libs/cli/tests/unit_tests/cli/__init__.py b/langgraph/source/libs/cli/tests/unit_tests/cli/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/tests/unit_tests/cli/langgraph.json b/langgraph/source/libs/cli/tests/unit_tests/cli/langgraph.json
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/tests/unit_tests/cli/pyproject.toml b/langgraph/source/libs/cli/tests/unit_tests/cli/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/tests/unit_tests/cli/test_cli.py b/langgraph/source/libs/cli/tests/unit_tests/cli/test_cli.py
new file mode 100644
index 0000000000000000000000000000000000000000..58ef6834364fbac53ee153b5df3f8dbf1acfc44e
--- /dev/null
+++ b/langgraph/source/libs/cli/tests/unit_tests/cli/test_cli.py
@@ -0,0 +1,824 @@
+import json
+import pathlib
+import re
+import shutil
+import tempfile
+import textwrap
+from contextlib import contextmanager
+from pathlib import Path
+
+from click.testing import CliRunner
+
+from langgraph_cli.cli import cli, prepare_args_and_stdin
+from langgraph_cli.config import Config, _get_pip_cleanup_lines, validate_config
+from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version
+from langgraph_cli.util import clean_empty_lines
+
+FORMATTED_CLEANUP_LINES = _get_pip_cleanup_lines(
+ install_cmd="uv pip install --system",
+ to_uninstall=("pip", "setuptools", "wheel"),
+ pip_installer="uv",
+)
+DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities(
+ version_docker=Version(26, 1, 1),
+ version_compose=Version(2, 27, 0),
+ healthcheck_start_interval=True,
+)
+
+
+@contextmanager
+def temporary_config_folder(config_content: dict, levels: int = 0):
+ # Create a temporary directory
+ temp_dir = tempfile.mkdtemp()
+ try:
+ # Define the path for the config.json file
+ config_path = Path(temp_dir) / f"{'a/' * levels}config.json"
+ # Ensure the parent directory exists
+ config_path.parent.mkdir(parents=True, exist_ok=True)
+
+ # Write the provided dictionary content to config.json
+ with open(config_path, "w", encoding="utf-8") as config_file:
+ json.dump(config_content, config_file)
+
+ # Yield the temporary directory path for use within the context
+ yield config_path.parent
+ finally:
+ # Cleanup the temporary directory and its contents
+ shutil.rmtree(temp_dir)
+
+
+def test_prepare_args_and_stdin() -> None:
+ # this basically serves as an end-to-end test for using config and docker helpers
+ config_path = pathlib.Path(__file__).parent / "langgraph.json"
+ config = validate_config(
+ Config(dependencies=[".", "../../.."], graphs={"agent": "agent.py:graph"})
+ )
+ port = 8000
+ debugger_port = 8001
+ debugger_graph_url = f"http://127.0.0.1:{port}"
+
+ actual_args, actual_stdin = prepare_args_and_stdin(
+ capabilities=DEFAULT_DOCKER_CAPABILITIES,
+ config_path=config_path,
+ config=config,
+ docker_compose=pathlib.Path("custom-docker-compose.yml"),
+ port=port,
+ debugger_port=debugger_port,
+ debugger_base_url=debugger_graph_url,
+ watch=True,
+ )
+
+ expected_args = [
+ "--project-directory",
+ str(pathlib.Path(__file__).parent.absolute()),
+ "-f",
+ "custom-docker-compose.yml",
+ "-f",
+ "-",
+ ]
+ expected_stdin = f"""volumes:
+ langgraph-data:
+ driver: local
+services:
+ langgraph-redis:
+ image: redis:6
+ healthcheck:
+ test: redis-cli ping
+ interval: 5s
+ timeout: 1s
+ retries: 5
+ langgraph-postgres:
+ image: pgvector/pgvector:pg16
+ ports:
+ - "5433:5432"
+ environment:
+ POSTGRES_DB: postgres
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ command:
+ - postgres
+ - -c
+ - shared_preload_libraries=vector
+ volumes:
+ - langgraph-data:/var/lib/postgresql/data
+ healthcheck:
+ test: pg_isready -U postgres
+ start_period: 10s
+ timeout: 1s
+ retries: 5
+ interval: 60s
+ start_interval: 1s
+ langgraph-debugger:
+ image: langchain/langgraph-debugger
+ restart: on-failure
+ depends_on:
+ langgraph-postgres:
+ condition: service_healthy
+ ports:
+ - "{debugger_port}:3968"
+ environment:
+ VITE_STUDIO_LOCAL_GRAPH_URL: {debugger_graph_url}
+ langgraph-api:
+ ports:
+ - "8000:8000"
+ depends_on:
+ langgraph-redis:
+ condition: service_healthy
+ langgraph-postgres:
+ condition: service_healthy
+ environment:
+ REDIS_URI: redis://langgraph-redis:6379
+ POSTGRES_URI: {DEFAULT_POSTGRES_URI}
+ healthcheck:
+ test: python /api/healthcheck.py
+ interval: 60s
+ start_interval: 1s
+ start_period: 10s
+
+ pull_policy: build
+ build:
+ context: .
+ additional_contexts:
+ - cli_1: {str(pathlib.Path(__file__).parent.parent.parent.parent.absolute())}
+ dockerfile_inline: |
+ # syntax=docker/dockerfile:1.4
+ FROM langchain/langgraph-api:3.11
+ # -- Adding local package . --
+ ADD . /deps/cli
+ # -- End of local package . --
+ # -- Adding local package ../../.. --
+ COPY --from=cli_1 . /deps/cli_1
+ # -- End of local package ../../.. --
+ # -- Installing all local dependencies --
+ RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
+ # -- End of local dependencies install --
+ ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
+{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
+ WORKDIR /deps/cli
+
+ develop:
+ watch:
+ - path: langgraph.json
+ action: rebuild
+ - path: .
+ action: rebuild
+ - path: ../../..
+ action: rebuild\
+"""
+ assert actual_args == expected_args
+ assert clean_empty_lines(actual_stdin) == expected_stdin
+
+
+def test_prepare_args_and_stdin_with_image() -> None:
+ # this basically serves as an end-to-end test for using config and docker helpers
+ config_path = pathlib.Path(__file__).parent / "langgraph.json"
+ config = validate_config(
+ Config(dependencies=[".", "../../.."], graphs={"agent": "agent.py:graph"})
+ )
+ port = 8000
+ debugger_port = 8001
+ debugger_graph_url = f"http://127.0.0.1:{port}"
+
+ actual_args, actual_stdin = prepare_args_and_stdin(
+ capabilities=DEFAULT_DOCKER_CAPABILITIES,
+ config_path=config_path,
+ config=config,
+ docker_compose=pathlib.Path("custom-docker-compose.yml"),
+ port=port,
+ debugger_port=debugger_port,
+ debugger_base_url=debugger_graph_url,
+ watch=True,
+ image="my-cool-image",
+ )
+
+ expected_args = [
+ "--project-directory",
+ str(pathlib.Path(__file__).parent.absolute()),
+ "-f",
+ "custom-docker-compose.yml",
+ "-f",
+ "-",
+ ]
+ expected_stdin = f"""volumes:
+ langgraph-data:
+ driver: local
+services:
+ langgraph-redis:
+ image: redis:6
+ healthcheck:
+ test: redis-cli ping
+ interval: 5s
+ timeout: 1s
+ retries: 5
+ langgraph-postgres:
+ image: pgvector/pgvector:pg16
+ ports:
+ - "5433:5432"
+ environment:
+ POSTGRES_DB: postgres
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ command:
+ - postgres
+ - -c
+ - shared_preload_libraries=vector
+ volumes:
+ - langgraph-data:/var/lib/postgresql/data
+ healthcheck:
+ test: pg_isready -U postgres
+ start_period: 10s
+ timeout: 1s
+ retries: 5
+ interval: 60s
+ start_interval: 1s
+ langgraph-debugger:
+ image: langchain/langgraph-debugger
+ restart: on-failure
+ depends_on:
+ langgraph-postgres:
+ condition: service_healthy
+ ports:
+ - "{debugger_port}:3968"
+ environment:
+ VITE_STUDIO_LOCAL_GRAPH_URL: {debugger_graph_url}
+ langgraph-api:
+ ports:
+ - "8000:8000"
+ depends_on:
+ langgraph-redis:
+ condition: service_healthy
+ langgraph-postgres:
+ condition: service_healthy
+ environment:
+ REDIS_URI: redis://langgraph-redis:6379
+ POSTGRES_URI: {DEFAULT_POSTGRES_URI}
+ image: my-cool-image
+ healthcheck:
+ test: python /api/healthcheck.py
+ interval: 60s
+ start_interval: 1s
+ start_period: 10s
+
+
+ develop:
+ watch:
+ - path: langgraph.json
+ action: rebuild
+ - path: .
+ action: rebuild
+ - path: ../../..
+ action: rebuild\
+"""
+ assert actual_args == expected_args
+ assert clean_empty_lines(actual_stdin) == expected_stdin
+
+
+def test_version_option() -> None:
+ """Test the --version option of the CLI."""
+ runner = CliRunner()
+ result = runner.invoke(cli, ["--version"])
+
+ # Verify that the command executed successfully
+ assert result.exit_code == 0, "Expected exit code 0 for --version option"
+
+ # Check that the output contains the correct version information
+ assert "LangGraph CLI, version" in result.output, (
+ "Expected version information in output"
+ )
+
+
+def test_dockerfile_command_basic() -> None:
+ """Test the 'dockerfile' command with basic configuration."""
+ runner = CliRunner()
+ config_content = {
+ "python_version": "3.11",
+ "graphs": {"agent": "agent.py:graph"},
+ "dependencies": ["."],
+ }
+
+ with temporary_config_folder(config_content) as temp_dir:
+ save_path = temp_dir / "Dockerfile"
+
+ result = runner.invoke(
+ cli,
+ ["dockerfile", str(save_path), "--config", str(temp_dir / "config.json")],
+ )
+
+ # Assert command was successful
+ assert result.exit_code == 0, result.output
+ assert "✅ Created: Dockerfile" in result.output
+
+ # Check if Dockerfile was created
+ assert save_path.exists()
+
+
+def test_dockerfile_command_new_style_config() -> None:
+ """Test `dockerfile` command with a new style config.
+
+ This config format allows specifying agent data as a dictionary.
+ {
+ "graphs": {
+ "agent1": {
+ "path": ... # path to graph definition,
+ ... # other fields
+ }
+ }
+ }
+ """
+ runner = CliRunner()
+ config_content = {
+ "dependencies": ["./my_agent"],
+ "graphs": {
+ "agent": {
+ "path": "./my_agent/agent.py:graph",
+ "description": "This is a test agent",
+ }
+ },
+ "env": ".env",
+ }
+ with temporary_config_folder(config_content) as temp_dir:
+ save_path = temp_dir / "Dockerfile"
+ # Add agent.py file
+ agent_path = temp_dir / "my_agent" / "agent.py"
+ agent_path.parent.mkdir(parents=True, exist_ok=True)
+ agent_path.touch()
+
+ result = runner.invoke(
+ cli,
+ ["dockerfile", str(save_path), "--config", str(temp_dir / "config.json")],
+ )
+
+ # Assert command was successful
+ assert result.exit_code == 0, result.output
+ assert "✅ Created: Dockerfile" in result.output
+
+ # Check if Dockerfile was created
+ assert save_path.exists()
+
+
+def test_dockerfile_command_with_base_image() -> None:
+ """Test the 'dockerfile' command with a base image."""
+ runner = CliRunner()
+ config_content = {
+ "python_version": "3.11",
+ "graphs": {"agent": "agent.py:graph"},
+ "dependencies": ["."],
+ "base_image": "langchain/langgraph-server:0.2",
+ }
+ with temporary_config_folder(config_content) as temp_dir:
+ save_path = temp_dir / "Dockerfile"
+ agent_path = temp_dir / "agent.py"
+ agent_path.parent.mkdir(parents=True, exist_ok=True)
+ agent_path.touch()
+
+ result = runner.invoke(
+ cli,
+ ["dockerfile", str(save_path), "--config", str(temp_dir / "config.json")],
+ )
+
+ assert result.exit_code == 0, result.output
+ assert "✅ Created: Dockerfile" in result.output
+
+ assert save_path.exists()
+ with open(save_path) as f:
+ dockerfile = f.read()
+ assert re.match("FROM langchain/langgraph-server:0.2-py3.*", dockerfile), (
+ "\n".join(dockerfile.splitlines()[:3])
+ )
+
+
+def test_dockerfile_command_with_docker_compose() -> None:
+ """Test the 'dockerfile' command with Docker Compose configuration."""
+ runner = CliRunner()
+ config_content = {
+ "dependencies": ["./my_agent"],
+ "graphs": {"agent": "./my_agent/agent.py:graph"},
+ "env": ".env",
+ }
+ with temporary_config_folder(config_content) as temp_dir:
+ save_path = temp_dir / "Dockerfile"
+ # Add agent.py file
+ agent_path = temp_dir / "my_agent" / "agent.py"
+ agent_path.parent.mkdir(parents=True, exist_ok=True)
+ agent_path.touch()
+
+ result = runner.invoke(
+ cli,
+ [
+ "dockerfile",
+ str(save_path),
+ "--config",
+ str(temp_dir / "config.json"),
+ "--add-docker-compose",
+ ],
+ )
+
+ # Assert command was successful
+ assert result.exit_code == 0
+ assert "✅ Created: Dockerfile" in result.output
+ assert "✅ Created: .dockerignore" in result.output
+ assert "✅ Created: docker-compose.yml" in result.output
+ assert (
+ "✅ Created: .env" in result.output or "➖ Skipped: .env" in result.output
+ )
+ assert "🎉 Files generated successfully" in result.output
+
+ # Check if Dockerfile, .dockerignore, docker-compose.yml, and .env were created
+ assert save_path.exists()
+ assert (temp_dir / ".dockerignore").exists()
+ assert (temp_dir / "docker-compose.yml").exists()
+ assert (temp_dir / ".env").exists() or "➖ Skipped: .env" in result.output
+
+
+def test_dockerfile_command_with_bad_config() -> None:
+ """Test the 'dockerfile' command with basic configuration."""
+ runner = CliRunner()
+ config_content = {
+ "node_version": "20" # Add any other necessary configuration fields
+ }
+
+ with temporary_config_folder(config_content) as temp_dir:
+ save_path = temp_dir / "Dockerfile"
+
+ result = runner.invoke(
+ cli,
+ ["dockerfile", str(save_path), "--config", str(temp_dir / "conf.json")],
+ )
+
+ # Assert command was successful
+ assert result.exit_code == 2
+ assert "conf.json' does not exist" in result.output
+
+
+def test_dockerfile_command_shows_wolfi_warning() -> None:
+ """Test the 'dockerfile' command shows warning when image_distro is not wolfi."""
+ runner = CliRunner()
+ config_content = {
+ "python_version": "3.11",
+ "graphs": {"agent": "agent.py:graph"},
+ "dependencies": ["."],
+ # No image_distro specified - should default to debian and show warning
+ }
+
+ with temporary_config_folder(config_content) as temp_dir:
+ save_path = temp_dir / "Dockerfile"
+ agent_path = temp_dir / "agent.py"
+ agent_path.touch()
+
+ result = runner.invoke(
+ cli,
+ ["dockerfile", str(save_path), "--config", str(temp_dir / "config.json")],
+ )
+
+ # Assert command was successful
+ assert result.exit_code == 0, result.output
+
+ # Check that warning is shown
+ assert "Security Recommendation" in result.output
+ assert "Wolfi Linux" in result.output
+ assert "image_distro" in result.output
+ assert "wolfi" in result.output
+
+
+def test_dockerfile_command_no_wolfi_warning_when_wolfi_set() -> None:
+ """Test the 'dockerfile' command does NOT show warning when image_distro is wolfi."""
+ runner = CliRunner()
+ config_content = {
+ "python_version": "3.11",
+ "graphs": {"agent": "agent.py:graph"},
+ "dependencies": ["."],
+ "image_distro": "wolfi", # Explicitly set to wolfi - should not show warning
+ }
+
+ with temporary_config_folder(config_content) as temp_dir:
+ save_path = temp_dir / "Dockerfile"
+ agent_path = temp_dir / "agent.py"
+ agent_path.touch()
+
+ result = runner.invoke(
+ cli,
+ ["dockerfile", str(save_path), "--config", str(temp_dir / "config.json")],
+ )
+
+ # Assert command was successful
+ assert result.exit_code == 0, result.output
+
+ # Check that warning is NOT shown
+ assert "Security Recommendation" not in result.output
+ assert "Wolfi Linux" not in result.output
+
+
+def test_build_command_shows_wolfi_warning() -> None:
+ """Test the 'build' command shows warning when image_distro is not wolfi."""
+ runner = CliRunner()
+ config_content = {
+ "python_version": "3.11",
+ "graphs": {"agent": "agent.py:graph"},
+ "dependencies": ["."],
+ # No image_distro specified - should default to debian and show warning
+ }
+
+ with temporary_config_folder(config_content) as temp_dir:
+ agent_path = temp_dir / "agent.py"
+ agent_path.touch()
+
+ # Mock docker command since we don't want to actually build
+ with runner.isolated_filesystem():
+ result = runner.invoke(
+ cli,
+ [
+ "build",
+ "--tag",
+ "test-image",
+ "--config",
+ str(temp_dir / "config.json"),
+ ],
+ catch_exceptions=True,
+ )
+
+ # The command will fail because docker isn't available or we're mocking,
+ # but we should still see the warning before it fails
+ assert "Security Recommendation" in result.output
+ assert "Wolfi Linux" in result.output
+ assert "image_distro" in result.output
+ assert "wolfi" in result.output
+
+
+def test_build_generate_proper_build_context():
+ runner = CliRunner()
+ config_content = {
+ "python_version": "3.11",
+ "graphs": {"agent": "agent.py:graph"},
+ "dependencies": [".", "../../..", "../.."],
+ "image_distro": "wolfi",
+ }
+
+ with temporary_config_folder(config_content, levels=3) as temp_dir:
+ agent_path = temp_dir / "agent.py"
+ agent_path.touch()
+
+ # Mock docker command since we don't want to actually build
+ with runner.isolated_filesystem():
+ result = runner.invoke(
+ cli,
+ [
+ "build",
+ "--tag",
+ "test-image",
+ "--config",
+ str(temp_dir / "config.json"),
+ ],
+ catch_exceptions=True,
+ )
+
+ build_context_pattern = re.compile(r"--build-context\s+([\w-]+)=([^\s]+)")
+
+ build_contexts = re.findall(build_context_pattern, result.output)
+ assert len(build_contexts) == 2, (
+ f"Expected 2 build contexts, but found {len(build_contexts)}"
+ )
+
+
+def test_dockerfile_command_with_api_version() -> None:
+ """Test the 'dockerfile' command with --api-version flag."""
+ runner = CliRunner()
+ config_content = {
+ "python_version": "3.11",
+ "graphs": {"agent": "agent.py:graph"},
+ "dependencies": ["."],
+ }
+
+ with temporary_config_folder(config_content) as temp_dir:
+ save_path = temp_dir / "Dockerfile"
+ agent_path = temp_dir / "agent.py"
+ agent_path.touch()
+
+ result = runner.invoke(
+ cli,
+ [
+ "dockerfile",
+ str(save_path),
+ "--config",
+ str(temp_dir / "config.json"),
+ "--api-version",
+ "0.2.74",
+ ],
+ )
+
+ # Assert command was successful
+ assert result.exit_code == 0, result.output
+ assert "✅ Created: Dockerfile" in result.output
+
+ # Check if Dockerfile was created and contains correct FROM line
+ assert save_path.exists()
+ with open(save_path) as f:
+ dockerfile = f.read()
+ assert "FROM langchain/langgraph-api:0.2.74-py3.11" in dockerfile
+
+
+def test_dockerfile_command_with_api_version_and_base_image() -> None:
+ """Test the 'dockerfile' command with both --api-version and --base-image flags."""
+ runner = CliRunner()
+ config_content = {
+ "python_version": "3.12",
+ "graphs": {"agent": "agent.py:graph"},
+ "dependencies": ["."],
+ "image_distro": "wolfi",
+ }
+
+ with temporary_config_folder(config_content) as temp_dir:
+ save_path = temp_dir / "Dockerfile"
+ agent_path = temp_dir / "agent.py"
+ agent_path.touch()
+
+ result = runner.invoke(
+ cli,
+ [
+ "dockerfile",
+ str(save_path),
+ "--config",
+ str(temp_dir / "config.json"),
+ "--api-version",
+ "1.0.0",
+ "--base-image",
+ "my-registry/custom-api",
+ ],
+ )
+
+ # Assert command was successful
+ assert result.exit_code == 0, result.output
+ assert "✅ Created: Dockerfile" in result.output
+
+ # Check if Dockerfile was created and contains correct FROM line
+ assert save_path.exists()
+ with open(save_path) as f:
+ dockerfile = f.read()
+ assert "FROM my-registry/custom-api:1.0.0-py3.12-wolfi" in dockerfile
+
+
+def test_dockerfile_command_with_api_version_nodejs() -> None:
+ """Test the 'dockerfile' command with --api-version flag for Node.js config."""
+ runner = CliRunner()
+ config_content = {
+ "node_version": "20",
+ "graphs": {"agent": "agent.js:graph"},
+ }
+
+ with temporary_config_folder(config_content) as temp_dir:
+ save_path = temp_dir / "Dockerfile"
+ agent_path = temp_dir / "agent.js"
+ agent_path.touch()
+
+ result = runner.invoke(
+ cli,
+ [
+ "dockerfile",
+ str(save_path),
+ "--config",
+ str(temp_dir / "config.json"),
+ "--api-version",
+ "0.2.74",
+ ],
+ )
+
+ # Assert command was successful
+ assert result.exit_code == 0, result.output
+ assert "✅ Created: Dockerfile" in result.output
+
+ # Check if Dockerfile was created and contains correct FROM line
+ assert save_path.exists()
+ with open(save_path) as f:
+ dockerfile = f.read()
+ assert "FROM langchain/langgraphjs-api:0.2.74-node20" in dockerfile
+
+
+def test_build_command_with_api_version() -> None:
+ """Test the 'build' command with --api-version flag."""
+ runner = CliRunner()
+ config_content = {
+ "python_version": "3.11",
+ "graphs": {"agent": "agent.py:graph"},
+ "dependencies": ["."],
+ "image_distro": "wolfi", # Use wolfi to avoid warning messages
+ }
+
+ with temporary_config_folder(config_content) as temp_dir:
+ agent_path = temp_dir / "agent.py"
+ agent_path.touch()
+
+ # Mock docker command since we don't want to actually build
+ with runner.isolated_filesystem():
+ result = runner.invoke(
+ cli,
+ [
+ "build",
+ "--tag",
+ "test-image",
+ "--config",
+ str(temp_dir / "config.json"),
+ "--api-version",
+ "0.2.74",
+ "--no-pull", # Avoid pulling non-existent images
+ ],
+ catch_exceptions=True,
+ )
+
+ # Check that the build command is called with the correct tag
+ # The output should contain the docker build command with the api_version tag
+ assert "langchain/langgraph-api:0.2.74-py3.11-wolfi" in result.output
+
+
+def test_build_command_with_api_version_and_base_image() -> None:
+ """Test the 'build' command with both --api-version and --base-image flags."""
+ runner = CliRunner()
+ config_content = {
+ "python_version": "3.12",
+ "graphs": {"agent": "agent.py:graph"},
+ "dependencies": ["."],
+ "image_distro": "wolfi", # Use wolfi to avoid warning messages
+ }
+
+ with temporary_config_folder(config_content) as temp_dir:
+ agent_path = temp_dir / "agent.py"
+ agent_path.touch()
+
+ # Mock docker command since we don't want to actually build
+ with runner.isolated_filesystem():
+ result = runner.invoke(
+ cli,
+ [
+ "build",
+ "--tag",
+ "test-image",
+ "--config",
+ str(temp_dir / "config.json"),
+ "--api-version",
+ "1.0.0",
+ "--base-image",
+ "my-registry/custom-api",
+ "--no-pull", # Avoid pulling non-existent images
+ ],
+ catch_exceptions=True,
+ )
+
+ # Check that the build command includes the api_version
+ assert "my-registry/custom-api:1.0.0-py3.12-wolfi" in result.output
+
+
+def test_prepare_args_and_stdin_with_api_version() -> None:
+ """Test prepare_args_and_stdin function with api_version parameter."""
+ config_path = pathlib.Path(__file__).parent / "langgraph.json"
+ config = validate_config(
+ Config(dependencies=["."], graphs={"agent": "agent.py:graph"})
+ )
+ port = 8000
+ api_version = "0.2.74"
+
+ actual_args, actual_stdin = prepare_args_and_stdin(
+ capabilities=DEFAULT_DOCKER_CAPABILITIES,
+ config_path=config_path,
+ config=config,
+ docker_compose=None,
+ port=port,
+ watch=False,
+ api_version=api_version,
+ )
+
+ expected_args = [
+ "--project-directory",
+ str(pathlib.Path(__file__).parent.absolute()),
+ "-f",
+ "-",
+ ]
+
+ # Check that the args are correct
+ assert actual_args == expected_args
+
+ # Check that the stdin contains the correct FROM line with api_version
+ assert "FROM langchain/langgraph-api:0.2.74-py3.11" in actual_stdin
+
+
+def test_prepare_args_and_stdin_with_api_version_and_image() -> None:
+ """Test prepare_args_and_stdin function with both api_version and image parameters."""
+ config_path = pathlib.Path(__file__).parent / "langgraph.json"
+ config = validate_config(
+ Config(dependencies=["."], graphs={"agent": "agent.py:graph"})
+ )
+ port = 8000
+ api_version = "0.2.74"
+ image = "my-custom-image:latest"
+
+ actual_args, actual_stdin = prepare_args_and_stdin(
+ capabilities=DEFAULT_DOCKER_CAPABILITIES,
+ config_path=config_path,
+ config=config,
+ docker_compose=None,
+ port=port,
+ watch=False,
+ api_version=api_version,
+ image=image,
+ )
+
+ # When image is provided, api_version should be ignored for the image
+ # but the stdin should not contain a build section (since image is provided)
+ assert "pull_policy: build" not in actual_stdin
diff --git a/langgraph/source/libs/cli/tests/unit_tests/cli/test_templates.py b/langgraph/source/libs/cli/tests/unit_tests/cli/test_templates.py
new file mode 100644
index 0000000000000000000000000000000000000000..cbdf4bbe1b6e4090e617a4deb24ec7fc23a98e50
--- /dev/null
+++ b/langgraph/source/libs/cli/tests/unit_tests/cli/test_templates.py
@@ -0,0 +1,70 @@
+"""Unit tests for the 'new' CLI command.
+
+This command creates a new LangGraph project using a specified template.
+"""
+
+import os
+from io import BytesIO
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from unittest.mock import MagicMock, patch
+from urllib import request
+from zipfile import ZipFile
+
+from click.testing import CliRunner
+
+from langgraph_cli.cli import cli
+from langgraph_cli.templates import TEMPLATE_ID_TO_CONFIG
+
+
+@patch.object(request, "urlopen")
+def test_create_new_with_mocked_download(mock_urlopen: MagicMock) -> None:
+ """Test the 'new' CLI command with a mocked download response using urllib."""
+ # Mock the response content to simulate a ZIP file
+ mock_zip_content = BytesIO()
+ with ZipFile(mock_zip_content, "w") as mock_zip:
+ mock_zip.writestr("test-file.txt", "Test content.")
+
+ # Create a mock response that behaves like a context manager
+ mock_response = MagicMock()
+ mock_response.read.return_value = mock_zip_content.getvalue()
+ mock_response.__enter__.return_value = mock_response # Setup enter context
+ mock_response.status = 200
+
+ mock_urlopen.return_value = mock_response
+
+ with TemporaryDirectory() as temp_dir:
+ runner = CliRunner()
+ template = next(
+ iter(TEMPLATE_ID_TO_CONFIG)
+ ) # Select the first template for the test
+ result = runner.invoke(cli, ["new", temp_dir, "--template", template])
+
+ # Verify CLI command execution and success
+ assert result.exit_code == 0, result.output
+ assert "New project created" in result.output, (
+ "Expected success message in output."
+ )
+
+ # Verify that the directory is not empty
+ assert os.listdir(temp_dir), "Expected files to be created in temp directory."
+
+ # Check for a known file in the extracted content
+ extracted_files = [f.name for f in Path(temp_dir).glob("*")]
+ assert "test-file.txt" in extracted_files, (
+ "Expected 'test-file.txt' in the extracted content."
+ )
+
+
+def test_invalid_template_id() -> None:
+ """Test that an invalid template ID passed via CLI results in a graceful error."""
+ runner = CliRunner()
+ result = runner.invoke(
+ cli, ["new", "dummy_path", "--template", "invalid-template-id"]
+ )
+
+ # Verify the command failed and proper message is displayed
+ assert result.exit_code != 0, "Expected non-zero exit code for invalid template."
+ assert "Template 'invalid-template-id' not found" in result.output, (
+ "Expected error message in output."
+ )
diff --git a/langgraph/source/libs/cli/tests/unit_tests/conftest.py b/langgraph/source/libs/cli/tests/unit_tests/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..5caaa6c81227787551fde9264cbd6246359d0468
--- /dev/null
+++ b/langgraph/source/libs/cli/tests/unit_tests/conftest.py
@@ -0,0 +1,16 @@
+import os
+from unittest.mock import patch
+
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def disable_analytics_env() -> None:
+ """Disable analytics for unit tests LANGGRAPH_CLI_NO_ANALYTICS."""
+ # First check if the environment variable is already set, if so, log a warning prior
+ # to overriding it.
+ if "LANGGRAPH_CLI_NO_ANALYTICS" in os.environ:
+ print("⚠️ LANGGRAPH_CLI_NO_ANALYTICS is set. Overriding it for the test.")
+
+ with patch.dict(os.environ, {"LANGGRAPH_CLI_NO_ANALYTICS": "0"}):
+ yield
diff --git a/langgraph/source/libs/cli/tests/unit_tests/graphs/agent.py b/langgraph/source/libs/cli/tests/unit_tests/graphs/agent.py
new file mode 100644
index 0000000000000000000000000000000000000000..700ba9fcb45b6304cc8a053552c0e538d88d2f90
--- /dev/null
+++ b/langgraph/source/libs/cli/tests/unit_tests/graphs/agent.py
@@ -0,0 +1,6 @@
+from langgraph.func import entrypoint
+
+
+@entrypoint()
+def graph(state):
+ return None
diff --git a/langgraph/source/libs/cli/tests/unit_tests/helpers.py b/langgraph/source/libs/cli/tests/unit_tests/helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..79b67a2c937e6160833f2589546b9bd4180db0b4
--- /dev/null
+++ b/langgraph/source/libs/cli/tests/unit_tests/helpers.py
@@ -0,0 +1,2 @@
+def clean_empty_lines(input_str: str):
+ return "\n".join(filter(None, input_str.splitlines()))
diff --git a/langgraph/source/libs/cli/tests/unit_tests/multiplatform/js.mts b/langgraph/source/libs/cli/tests/unit_tests/multiplatform/js.mts
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/tests/unit_tests/multiplatform/python.py b/langgraph/source/libs/cli/tests/unit_tests/multiplatform/python.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/tests/unit_tests/pipconfig.txt b/langgraph/source/libs/cli/tests/unit_tests/pipconfig.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/cli/tests/unit_tests/test_config.json b/langgraph/source/libs/cli/tests/unit_tests/test_config.json
new file mode 100644
index 0000000000000000000000000000000000000000..3ec8f0908ec9348fb4670abaf216e308df534b9e
--- /dev/null
+++ b/langgraph/source/libs/cli/tests/unit_tests/test_config.json
@@ -0,0 +1,19 @@
+{
+ "python_version": "3.12",
+ "pip_config_file": "pipconfig.txt",
+ "dockerfile_lines": [
+ "ARG meow=woof"
+ ],
+ "dependencies": [
+ "langchain_openai",
+ "starlette",
+ "."
+ ],
+ "graphs": {
+ "agent": "graphs/agent.py:graph"
+ },
+ "env": ".env",
+ "http": {
+ "app": "../../examples/my_app.py:app"
+ }
+}
diff --git a/langgraph/source/libs/cli/tests/unit_tests/test_config.py b/langgraph/source/libs/cli/tests/unit_tests/test_config.py
new file mode 100644
index 0000000000000000000000000000000000000000..566ba272f643f603a1e33d0caa5667be41d49c87
--- /dev/null
+++ b/langgraph/source/libs/cli/tests/unit_tests/test_config.py
@@ -0,0 +1,1683 @@
+import copy
+import json
+import os
+import pathlib
+import tempfile
+import textwrap
+
+import click
+import pytest
+
+from langgraph_cli.config import (
+ _BUILD_TOOLS,
+ _get_pip_cleanup_lines,
+ config_to_compose,
+ config_to_docker,
+ docker_tag,
+ validate_config,
+ validate_config_file,
+)
+from langgraph_cli.util import clean_empty_lines
+
+FORMATTED_CLEANUP_LINES = _get_pip_cleanup_lines(
+ install_cmd="uv pip install --system",
+ to_uninstall=("pip", "setuptools", "wheel"),
+ pip_installer="uv",
+)
+
+PATH_TO_CONFIG = pathlib.Path(__file__).parent / "test_config.json"
+
+
+def test_validate_config():
+ # minimal config
+ expected_config = {
+ "dependencies": ["."],
+ "graphs": {
+ "agent": "./agent.py:graph",
+ },
+ }
+ actual_config = validate_config(expected_config)
+ expected_config = {
+ "base_image": None,
+ "python_version": "3.11",
+ "node_version": None,
+ "pip_config_file": None,
+ "pip_installer": "auto",
+ "image_distro": "debian",
+ "dockerfile_lines": [],
+ "env": {},
+ "store": None,
+ "auth": None,
+ "encryption": None,
+ "webhooks": None,
+ "checkpointer": None,
+ "http": None,
+ "ui": None,
+ "ui_config": None,
+ "keep_pkg_tools": None,
+ **expected_config,
+ }
+ assert actual_config == expected_config
+
+ # full config
+ env = ".env"
+ expected_config = {
+ "base_image": None,
+ "python_version": "3.12",
+ "node_version": None,
+ "pip_config_file": "pipconfig.txt",
+ "pip_installer": "auto",
+ "image_distro": "debian",
+ "dockerfile_lines": ["ARG meow"],
+ "dependencies": [".", "langchain"],
+ "graphs": {
+ "agent": "./agent.py:graph",
+ },
+ "env": env,
+ "store": None,
+ "auth": None,
+ "encryption": None,
+ "webhooks": None,
+ "checkpointer": None,
+ "http": None,
+ "ui": None,
+ "ui_config": None,
+ "keep_pkg_tools": None,
+ }
+ 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):
+ validate_config({"python_version": "3.9"})
+
+ # check missing dependencies key raises
+ with pytest.raises(click.UsageError):
+ validate_config(
+ {"python_version": "3.9", "graphs": {"agent": "./agent.py:graph"}}
+ )
+
+ # check missing graphs key raises
+ 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 = validate_config(
+ {
+ "python_version": "3.11-bullseye",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ }
+ )
+ assert config["python_version"] == "3.11-bullseye"
+
+ config = validate_config(
+ {
+ "python_version": "3.12-slim",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ }
+ )
+ assert config["python_version"] == "3.12-slim"
+ with pytest.raises(ValueError, match="Invalid http.app format"):
+ validate_config(
+ {
+ "python_version": "3.12",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "http": {"app": "../../examples/my_app.py"},
+ }
+ )
+
+
+def test_validate_config_image_distro():
+ """Test validation of image_distro field."""
+ # Valid image_distro values should work
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "image_distro": "debian",
+ }
+ )
+ assert config["image_distro"] == "debian"
+
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "image_distro": "wolfi",
+ }
+ )
+ assert config["image_distro"] == "wolfi"
+
+ # Missing image_distro should default to 'debian'
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ }
+ )
+ assert config["image_distro"] == "debian"
+
+ # Invalid image_distro values should raise error
+ with pytest.raises(click.UsageError) as exc_info:
+ validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "image_distro": "ubuntu",
+ }
+ )
+ assert "Invalid image_distro: 'ubuntu'" in str(exc_info.value)
+
+ with pytest.raises(click.UsageError) as exc_info:
+ validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "image_distro": "alpine",
+ }
+ )
+ assert "Invalid image_distro: 'alpine'" in str(exc_info.value)
+
+ # Test base Node.js config with image distro
+ config = validate_config(
+ {
+ "node_version": "20",
+ "graphs": {"agent": "./agent.js:graph"},
+ "image_distro": "wolfi",
+ }
+ )
+ assert config["image_distro"] == "wolfi"
+
+ # Test Node.js config with no distro specified
+ config = validate_config(
+ {
+ "node_version": "20",
+ "graphs": {"agent": "./agent.js:graph"},
+ }
+ )
+ assert config["image_distro"] == "debian"
+
+
+def test_validate_config_pip_installer():
+ """Test validation of pip_installer field."""
+ # Valid pip_installer values should work
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "pip_installer": "auto",
+ }
+ )
+ assert config["pip_installer"] == "auto"
+
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "pip_installer": "pip",
+ }
+ )
+ assert config["pip_installer"] == "pip"
+
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "pip_installer": "uv",
+ }
+ )
+ assert config["pip_installer"] == "uv"
+
+ # Missing pip_installer should default to "auto"
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ }
+ )
+ assert config["pip_installer"] == "auto"
+
+ # Invalid pip_installer values should raise error
+ with pytest.raises(click.UsageError) as exc_info:
+ validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "pip_installer": "conda",
+ }
+ )
+ assert "Invalid pip_installer: 'conda'" in str(exc_info.value)
+ assert "Must be 'auto', 'pip', or 'uv'" in str(exc_info.value)
+
+ with pytest.raises(click.UsageError) as exc_info:
+ validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "pip_installer": "invalid",
+ }
+ )
+ assert "Invalid pip_installer: 'invalid'" 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)
+
+
+def test_validate_config_multiplatform():
+ # default node
+ config = validate_config(
+ {"dependencies": ["."], "graphs": {"js": "./js.mts:graph"}}
+ )
+ assert config["node_version"] == "20"
+ assert config["python_version"] is None
+
+ # default multiplatform
+ config = validate_config(
+ {
+ "node_version": "22",
+ "python_version": "3.12",
+ "dependencies": ["."],
+ "graphs": {"python": "./python.py:graph", "js": "./js.mts:graph"},
+ }
+ )
+ assert config["node_version"] == "22"
+ assert config["python_version"] == "3.12"
+
+ # default multiplatform (full infer)
+ graphs = {"python": "./python.py:graph", "js": "./js.mts:graph"}
+ config = validate_config({"dependencies": ["."], "graphs": graphs})
+ assert config["node_version"] == "20"
+ assert config["python_version"] == "3.11"
+
+ # default multiplatform (partial node)
+ config = validate_config(
+ {"node_version": "22", "dependencies": ["."], "graphs": graphs}
+ )
+ assert config["node_version"] == "22"
+ assert config["python_version"] == "3.11"
+
+ # default multiplatform (partial python)
+ config = validate_config(
+ {"python_version": "3.12", "dependencies": ["."], "graphs": graphs}
+ )
+ assert config["node_version"] == "20"
+ assert config["python_version"] == "3.12"
+
+ # no known extension (assumes python)
+ config = validate_config(
+ {
+ "dependencies": ["./local", "./shared_utils"],
+ "graphs": {"agent": "local.workflow:graph"},
+ "env": ".env",
+ }
+ )
+ assert config["node_version"] is None
+ assert config["python_version"] == "3.11"
+
+
+# config_to_docker
+def test_config_to_docker_simple():
+ graphs = {"agent": "./agent.py:graph"}
+ actual_docker_stdin, additional_contexts = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config(
+ {
+ "dependencies": [".", "../../examples/graphs_reqs_a", "../../examples"],
+ "graphs": graphs,
+ "http": {"app": "../../examples/my_app.py:app"},
+ }
+ ),
+ base_image="langchain/langgraph-api",
+ )
+ expected_docker_stdin = f"""\
+# syntax=docker/dockerfile:1.4
+FROM langchain/langgraph-api:3.11
+# -- Installing local requirements --
+COPY --from=outer-requirements.txt requirements.txt /deps/outer-graphs_reqs_a/graphs_reqs_a/requirements.txt
+RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -r /deps/outer-graphs_reqs_a/graphs_reqs_a/requirements.txt
+# -- End of local requirements install --
+# -- Adding local package ../../examples --
+COPY --from=examples . /deps/examples
+# -- End of local package ../../examples --
+# -- Adding non-package dependency unit_tests --
+ADD . /deps/outer-unit_tests/unit_tests
+RUN set -ex && \\
+ for line in '[project]' \\
+ 'name = "unit_tests"' \\
+ 'version = "0.1"' \\
+ '[tool.setuptools.package-data]' \\
+ '"*" = ["**/*"]' \\
+ '[build-system]' \\
+ 'requires = ["setuptools>=61"]' \\
+ 'build-backend = "setuptools.build_meta"'; do \\
+ echo "$line" >> /deps/outer-unit_tests/pyproject.toml; \\
+ done
+# -- End of non-package dependency unit_tests --
+# -- Adding non-package dependency graphs_reqs_a --
+COPY --from=outer-graphs_reqs_a . /deps/outer-graphs_reqs_a/graphs_reqs_a
+RUN set -ex && \\
+ for line in '[project]' \\
+ 'name = "graphs_reqs_a"' \\
+ 'version = "0.1"' \\
+ '[tool.setuptools.package-data]' \\
+ '"*" = ["**/*"]' \\
+ '[build-system]' \\
+ 'requires = ["setuptools>=61"]' \\
+ 'build-backend = "setuptools.build_meta"'; do \\
+ echo "$line" >> /deps/outer-graphs_reqs_a/pyproject.toml; \\
+ done
+# -- End of non-package dependency graphs_reqs_a --
+# -- Installing all local dependencies --
+RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
+# -- End of local dependencies install --
+ENV LANGGRAPH_HTTP='{{"app": "/deps/examples/my_app.py:app"}}'
+ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
+{FORMATTED_CLEANUP_LINES}
+WORKDIR /deps/outer-unit_tests/unit_tests\
+"""
+ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
+
+ assert additional_contexts == {
+ "outer-graphs_reqs_a": str(
+ (pathlib.Path(__file__).parent / "../../examples/graphs_reqs_a").resolve()
+ ),
+ "examples": str((pathlib.Path(__file__).parent / "../../examples").resolve()),
+ }
+
+
+def test_config_to_docker_outside_path():
+ graphs = {"agent": "./agent.py:graph"}
+ actual_docker_stdin, additional_contexts = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config({"dependencies": [".", ".."], "graphs": graphs}),
+ base_image="langchain/langgraph-api",
+ )
+ expected_docker_stdin = (
+ """\
+# syntax=docker/dockerfile:1.4
+FROM langchain/langgraph-api:3.11
+# -- Adding non-package dependency unit_tests --
+ADD . /deps/outer-unit_tests/unit_tests
+RUN set -ex && \\
+ for line in '[project]' \\
+ 'name = "unit_tests"' \\
+ 'version = "0.1"' \\
+ '[tool.setuptools.package-data]' \\
+ '"*" = ["**/*"]' \\
+ '[build-system]' \\
+ 'requires = ["setuptools>=61"]' \\
+ 'build-backend = "setuptools.build_meta"'; do \\
+ echo "$line" >> /deps/outer-unit_tests/pyproject.toml; \\
+ done
+# -- End of non-package dependency unit_tests --
+# -- Adding non-package dependency tests --
+COPY --from=outer-tests . /deps/outer-tests/tests
+RUN set -ex && \\
+ for line in '[project]' \\
+ 'name = "tests"' \\
+ 'version = "0.1"' \\
+ '[tool.setuptools.package-data]' \\
+ '"*" = ["**/*"]' \\
+ '[build-system]' \\
+ 'requires = ["setuptools>=61"]' \\
+ 'build-backend = "setuptools.build_meta"'; do \\
+ echo "$line" >> /deps/outer-tests/pyproject.toml; \\
+ done
+# -- End of non-package dependency tests --
+# -- Installing all local dependencies --
+RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
+# -- End of local dependencies install --
+ENV LANGSERVE_GRAPHS='{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}'
+"""
+ + FORMATTED_CLEANUP_LINES
+ + """
+WORKDIR /deps/outer-unit_tests/unit_tests\
+"""
+ )
+ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
+ assert additional_contexts == {
+ "outer-tests": str(pathlib.Path(__file__).parent.parent.absolute()),
+ }
+
+
+def test_config_to_docker_pipconfig():
+ graphs = {"agent": "./agent.py:graph"}
+ actual_docker_stdin, additional_contexts = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config(
+ {
+ "dependencies": ["."],
+ "graphs": graphs,
+ "pip_config_file": "pipconfig.txt",
+ }
+ ),
+ base_image="langchain/langgraph-api",
+ )
+ expected_docker_stdin = (
+ """\
+FROM langchain/langgraph-api:3.11
+ADD pipconfig.txt /pipconfig.txt
+# -- Adding non-package dependency unit_tests --
+ADD . /deps/outer-unit_tests/unit_tests
+RUN set -ex && \\
+ for line in '[project]' \\
+ 'name = "unit_tests"' \\
+ 'version = "0.1"' \\
+ '[tool.setuptools.package-data]' \\
+ '"*" = ["**/*"]' \\
+ '[build-system]' \\
+ 'requires = ["setuptools>=61"]' \\
+ 'build-backend = "setuptools.build_meta"'; do \\
+ echo "$line" >> /deps/outer-unit_tests/pyproject.toml; \\
+ done
+# -- End of non-package dependency unit_tests --
+# -- Installing all local dependencies --
+RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
+# -- End of local dependencies install --
+ENV LANGSERVE_GRAPHS='{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}'
+"""
+ + FORMATTED_CLEANUP_LINES
+ + """
+WORKDIR /deps/outer-unit_tests/unit_tests\
+"""
+ )
+ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
+ assert additional_contexts == {}
+
+
+def test_config_to_docker_invalid_inputs():
+ # test missing local dependencies
+ with pytest.raises(FileNotFoundError):
+ graphs = {"agent": "tests/unit_tests/agent.py:graph"}
+ config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config({"dependencies": ["./missing"], "graphs": graphs}),
+ base_image="langchain/langgraph-api",
+ )
+
+ # test missing local module
+ with pytest.raises(FileNotFoundError):
+ graphs = {"agent": "./missing_agent.py:graph"}
+ config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config({"dependencies": ["."], "graphs": graphs}),
+ base_image="langchain/langgraph-api",
+ )
+
+
+def test_config_to_docker_local_deps():
+ graphs = {"agent": "./graphs/agent.py:graph"}
+ actual_docker_stdin, additional_contexts = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config(
+ {
+ "dependencies": ["./graphs"],
+ "graphs": graphs,
+ }
+ ),
+ base_image="langchain/langgraph-api-custom",
+ )
+ expected_docker_stdin = f"""\
+FROM langchain/langgraph-api-custom:3.11
+# -- Adding non-package dependency graphs --
+ADD ./graphs /deps/outer-graphs/src
+RUN set -ex && \\
+ for line in '[project]' \\
+ 'name = "graphs"' \\
+ 'version = "0.1"' \\
+ '[tool.setuptools.package-data]' \\
+ '"*" = ["**/*"]' \\
+ '[build-system]' \\
+ 'requires = ["setuptools>=61"]' \\
+ 'build-backend = "setuptools.build_meta"'; do \\
+ echo "$line" >> /deps/outer-graphs/pyproject.toml; \\
+ done
+# -- End of non-package dependency graphs --
+# -- Installing all local dependencies --
+RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
+# -- End of local dependencies install --
+ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-graphs/src/agent.py:graph"}}'
+{FORMATTED_CLEANUP_LINES}\
+"""
+ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
+ assert additional_contexts == {}
+
+
+def test_config_to_docker_pyproject():
+ pyproject_str = """[project]
+name = "custom"
+version = "0.1"
+dependencies = ["langchain"]"""
+ pyproject_path = "tests/unit_tests/pyproject.toml"
+ with open(pyproject_path, "w") as f:
+ f.write(pyproject_str)
+
+ graphs = {"agent": "./graphs/agent.py:graph"}
+ actual_docker_stdin, additional_contexts = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config(
+ {
+ "dependencies": ["."],
+ "graphs": graphs,
+ }
+ ),
+ base_image="langchain/langgraph-api",
+ )
+ os.remove(pyproject_path)
+ expected_docker_stdin = (
+ """FROM langchain/langgraph-api:3.11
+# -- Adding local package . --
+ADD . /deps/unit_tests
+# -- End of local package . --
+# -- Installing all local dependencies --
+RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
+# -- End of local dependencies install --
+ENV LANGSERVE_GRAPHS='{"agent": "/deps/unit_tests/graphs/agent.py:graph"}'
+"""
+ + FORMATTED_CLEANUP_LINES
+ + "\n"
+ + "WORKDIR /deps/unit_tests"
+ ""
+ )
+ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
+ assert additional_contexts == {}
+
+
+def test_config_to_docker_end_to_end():
+ graphs = {"agent": "./graphs/agent.py:graph"}
+ actual_docker_stdin, additional_contexts = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config(
+ {
+ "python_version": "3.12",
+ "dependencies": ["./graphs/", "langchain", "langchain_openai"],
+ "graphs": graphs,
+ "pip_config_file": "pipconfig.txt",
+ "dockerfile_lines": ["ARG meow", "ARG foo"],
+ }
+ ),
+ base_image="langchain/langgraph-api",
+ )
+ expected_docker_stdin = f"""FROM langchain/langgraph-api:3.12
+ARG meow
+ARG foo
+ADD pipconfig.txt /pipconfig.txt
+RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt langchain langchain_openai
+# -- Adding non-package dependency graphs --
+ADD ./graphs/ /deps/outer-graphs/src
+RUN set -ex && \\
+ for line in '[project]' \\
+ 'name = "graphs"' \\
+ 'version = "0.1"' \\
+ '[tool.setuptools.package-data]' \\
+ '"*" = ["**/*"]' \\
+ '[build-system]' \\
+ 'requires = ["setuptools>=61"]' \\
+ 'build-backend = "setuptools.build_meta"'; do \\
+ echo "$line" >> /deps/outer-graphs/pyproject.toml; \\
+ done
+# -- End of non-package dependency graphs --
+# -- Installing all local dependencies --
+RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
+# -- End of local dependencies install --
+ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-graphs/src/agent.py:graph"}}'
+{FORMATTED_CLEANUP_LINES}"""
+ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
+ assert additional_contexts == {}
+
+
+# node.js build used for LangSmith Deployment
+def test_config_to_docker_nodejs():
+ graphs = {"agent": "./graphs/agent.js:graph"}
+ actual_docker_stdin, additional_contexts = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config(
+ {
+ "node_version": "20",
+ "graphs": graphs,
+ "dockerfile_lines": ["ARG meow", "ARG foo"],
+ "auth": {"path": "./graphs/auth.mts:auth"},
+ "ui": {"agent": "./graphs/agent.ui.jsx"},
+ "ui_config": {"shared": ["nuqs"]},
+ }
+ ),
+ base_image="langchain/langgraphjs-api",
+ )
+ expected_docker_stdin = """FROM langchain/langgraphjs-api:20
+ARG meow
+ARG foo
+ADD . /deps/unit_tests
+RUN cd /deps/unit_tests && npm i
+ENV LANGGRAPH_AUTH='{"path": "./graphs/auth.mts:auth"}'
+ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}'
+ENV LANGGRAPH_UI_CONFIG='{"shared": ["nuqs"]}'
+ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
+WORKDIR /deps/unit_tests
+RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
+
+ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
+ assert additional_contexts == {}
+
+
+def test_config_to_docker_python_encryption():
+ # Test that encryption config is included in validation
+ graphs = {"agent": "./agent.py:graph"}
+ validated = validate_config(
+ {
+ "python_version": "3.11",
+ "graphs": graphs,
+ "dependencies": ["."],
+ "encryption": {"path": "./encryption.py:encryption"},
+ }
+ )
+
+ # Verify that encryption config is preserved after validation
+ assert validated.get("encryption") is not None
+ assert validated["encryption"]["path"] == "./encryption.py:encryption"
+
+
+def test_config_to_docker_python_encryption_bad_path():
+ # Test that invalid encryption path format raises ValueError
+ graphs = {"agent": "./agent.py:graph"}
+ with pytest.raises(ValueError, match="Invalid encryption.path format"):
+ validate_config(
+ {
+ "python_version": "3.11",
+ "graphs": graphs,
+ "dependencies": ["."],
+ "encryption": {"path": "./encryption.py"}, # Missing :attribute
+ }
+ )
+
+
+def test_config_to_docker_python_encryption_formatted():
+ # Test that encryption config is properly formatted in Docker output
+ graphs = {"agent": "./graphs/agent.py:graph"}
+ actual_docker_stdin, _ = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": graphs,
+ "encryption": {"path": "./agent.py:my_encryption"},
+ }
+ ),
+ base_image="langchain/langgraph-api",
+ )
+ # Verify that LANGGRAPH_ENCRYPTION is in the docker output with the correct path
+ assert "LANGGRAPH_ENCRYPTION=" in actual_docker_stdin
+ assert (
+ "/deps/outer-unit_tests/unit_tests/agent.py:my_encryption"
+ in actual_docker_stdin
+ )
+
+
+def test_config_to_docker_nodejs_internal_docker_tag():
+ graphs = {"agent": "./graphs/agent.js:graph"}
+ actual_docker_stdin, additional_contexts = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config(
+ {
+ "node_version": "20",
+ "graphs": graphs,
+ "dockerfile_lines": ["ARG meow", "ARG foo"],
+ "auth": {"path": "./graphs/auth.mts:auth"},
+ "ui": {"agent": "./graphs/agent.ui.jsx"},
+ "ui_config": {"shared": ["nuqs"]},
+ "_INTERNAL_docker_tag": "my-tag",
+ }
+ ),
+ base_image="langchain/langgraphjs-api",
+ )
+ expected_docker_stdin = """FROM langchain/langgraphjs-api:my-tag
+ARG meow
+ARG foo
+ADD . /deps/unit_tests
+RUN cd /deps/unit_tests && npm i
+ENV LANGGRAPH_AUTH='{"path": "./graphs/auth.mts:auth"}'
+ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}'
+ENV LANGGRAPH_UI_CONFIG='{"shared": ["nuqs"]}'
+ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
+WORKDIR /deps/unit_tests
+RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
+
+ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
+ assert additional_contexts == {}
+
+
+def _extract_env_json(dockerfile: str, var_name: str) -> dict:
+ """Helper to extract and parse a JSON value from an ENV line in a Dockerfile."""
+ line_prefix = f"ENV {var_name}='"
+ for line in dockerfile.splitlines():
+ if line.startswith(line_prefix) and line.endswith("'"):
+ json_str = line[len(line_prefix) : -1]
+ return json.loads(json_str)
+ raise AssertionError(f"{var_name} not found in Dockerfile env lines")
+
+
+def test_config_to_docker_webhooks_python():
+ graphs = {"agent": "./agent.py:graph"}
+ webhooks = {
+ "env_prefix": "LG_WEBHOOK_",
+ "url": {
+ "require_https": True,
+ "allowed_domains": ["hooks.example.com", "*.example.org"],
+ "allowed_ports": [443],
+ "max_url_length": 1024,
+ "disable_loopback": False,
+ },
+ "headers": {
+ "x-auth": "${{ env.LG_WEBHOOK_TOKEN }}",
+ "x-mixed": "Bearer ${{ env.LG_WEBHOOK_TOKEN }}-suffix",
+ },
+ }
+
+ dockerfile, _ = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config(
+ {
+ "dependencies": ["."],
+ "graphs": graphs,
+ "webhooks": webhooks,
+ }
+ ),
+ base_image="langchain/langgraph-api",
+ )
+
+ # Ensure the ENV line is present and the payload round-trips via JSON
+ parsed = _extract_env_json(dockerfile, "LANGGRAPH_WEBHOOKS")
+ assert parsed == webhooks
+
+
+def test_config_to_docker_webhooks_node():
+ graphs = {"agent": "./graphs/agent.js:graph"}
+ webhooks = {
+ "env_prefix": "LG_WEBHOOK_",
+ "url": {"require_https": True},
+ "headers": {"x-auth": "${{ env.LG_WEBHOOK_TOKEN }}"},
+ }
+
+ dockerfile, _ = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config(
+ {
+ "node_version": "20",
+ "graphs": graphs,
+ "webhooks": webhooks,
+ }
+ ),
+ base_image="langchain/langgraphjs-api",
+ )
+
+ parsed = _extract_env_json(dockerfile, "LANGGRAPH_WEBHOOKS")
+ assert parsed == webhooks
+
+
+def test_config_to_docker_no_webhooks():
+ graphs = {"agent": "./agent.py:graph"}
+ dockerfile, _ = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config({"dependencies": ["."], "graphs": graphs}),
+ base_image="langchain/langgraph-api",
+ )
+
+ assert "ENV LANGGRAPH_WEBHOOKS=" not in dockerfile
+
+
+def test_config_to_docker_gen_ui_python():
+ graphs = {"agent": "./agent.py:graph"}
+ actual_docker_stdin, additional_contexts = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config(
+ {
+ "dependencies": ["."],
+ "graphs": graphs,
+ "ui": {"agent": "./graphs/agent.ui.jsx"},
+ "ui_config": {"shared": ["nuqs"]},
+ }
+ ),
+ base_image="langchain/langgraph-api",
+ )
+
+ expected_docker_stdin = f"""FROM langchain/langgraph-api:3.11
+RUN /storage/install-node.sh
+# -- Adding non-package dependency unit_tests --
+ADD . /deps/outer-unit_tests/unit_tests
+RUN set -ex && \\
+ for line in '[project]' \\
+ 'name = "unit_tests"' \\
+ 'version = "0.1"' \\
+ '[tool.setuptools.package-data]' \\
+ '"*" = ["**/*"]' \\
+ '[build-system]' \\
+ 'requires = ["setuptools>=61"]' \\
+ 'build-backend = "setuptools.build_meta"'; do \\
+ echo "$line" >> /deps/outer-unit_tests/pyproject.toml; \\
+ done
+# -- End of non-package dependency unit_tests --
+# -- Installing all local dependencies --
+RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
+# -- End of local dependencies install --
+ENV LANGGRAPH_UI='{{"agent": "./graphs/agent.ui.jsx"}}'
+ENV LANGGRAPH_UI_CONFIG='{{"shared": ["nuqs"]}}'
+ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
+# -- Installing JS dependencies --
+ENV NODE_VERSION=20
+RUN cd /deps/outer-unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts
+# -- End of JS dependencies install --
+{FORMATTED_CLEANUP_LINES}
+WORKDIR /deps/outer-unit_tests/unit_tests"""
+
+ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
+ assert additional_contexts == {}
+
+
+def test_config_to_docker_multiplatform():
+ graphs = {
+ "python": "./multiplatform/python.py:graph",
+ "js": "./multiplatform/js.mts:graph",
+ }
+ actual_docker_stdin, additional_contexts = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config(
+ {"node_version": "22", "dependencies": ["."], "graphs": graphs}
+ ),
+ base_image="langchain/langgraph-api",
+ )
+
+ expected_docker_stdin = f"""FROM langchain/langgraph-api:3.11
+RUN /storage/install-node.sh
+# -- Adding non-package dependency unit_tests --
+ADD . /deps/outer-unit_tests/unit_tests
+RUN set -ex && \\
+ for line in '[project]' \\
+ 'name = "unit_tests"' \\
+ 'version = "0.1"' \\
+ '[tool.setuptools.package-data]' \\
+ '"*" = ["**/*"]' \\
+ '[build-system]' \\
+ 'requires = ["setuptools>=61"]' \\
+ 'build-backend = "setuptools.build_meta"'; do \\
+ echo "$line" >> /deps/outer-unit_tests/pyproject.toml; \\
+ done
+# -- End of non-package dependency unit_tests --
+# -- Installing all local dependencies --
+RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
+# -- End of local dependencies install --
+ENV LANGSERVE_GRAPHS='{{"python": "/deps/outer-unit_tests/unit_tests/multiplatform/python.py:graph", "js": "/deps/outer-unit_tests/unit_tests/multiplatform/js.mts:graph"}}'
+# -- Installing JS dependencies --
+ENV NODE_VERSION=22
+RUN cd /deps/outer-unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts
+# -- End of JS dependencies install --
+{FORMATTED_CLEANUP_LINES}
+WORKDIR /deps/outer-unit_tests/unit_tests"""
+
+ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
+ assert additional_contexts == {}
+
+
+def test_config_to_docker_pip_installer():
+ """Test that pip_installer setting affects the generated Dockerfile."""
+ graphs = {"agent": "./graphs/agent.py:graph"}
+ base_config = {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": graphs,
+ }
+
+ # Test default (auto) behavior with UV-supporting image
+ config_auto = validate_config(
+ {**copy.deepcopy(base_config), "pip_installer": "auto"}
+ )
+ docker_auto, _ = config_to_docker(
+ PATH_TO_CONFIG, config_auto, base_image="langchain/langgraph-api:0.2.47"
+ )
+ assert "uv pip install --system " in docker_auto
+ assert "rm /usr/bin/uv /usr/bin/uvx" in docker_auto
+
+ # Test explicit pip setting
+ config_pip = validate_config({**copy.deepcopy(base_config), "pip_installer": "pip"})
+ docker_pip, _ = config_to_docker(
+ PATH_TO_CONFIG, config_pip, base_image="langchain/langgraph-api:0.2.47"
+ )
+ assert "uv pip install --system " not in docker_pip
+ assert "pip install" in docker_pip
+ assert "rm /usr/bin/uv" not in docker_pip
+
+ # Test explicit uv setting
+ config_uv = validate_config({**copy.deepcopy(base_config), "pip_installer": "uv"})
+ docker_uv, _ = config_to_docker(
+ PATH_TO_CONFIG, config_uv, base_image="langchain/langgraph-api:0.2.47"
+ )
+ assert "uv pip install --system " in docker_uv
+ assert "rm /usr/bin/uv /usr/bin/uvx" in docker_uv
+
+ # Test auto behavior with older image (should use pip)
+ config_auto_old = validate_config(
+ {**copy.deepcopy(base_config), "pip_installer": "auto"}
+ )
+ docker_auto_old, _ = config_to_docker(
+ PATH_TO_CONFIG, config_auto_old, base_image="langchain/langgraph-api:0.2.46"
+ )
+ assert "uv pip install --system " not in docker_auto_old
+ assert "pip install" in docker_auto_old
+ assert "rm /usr/bin/uv" not in docker_auto_old
+
+ # Test that missing pip_installer defaults to auto behavior
+ config_default = validate_config(copy.deepcopy(base_config))
+ docker_default, _ = config_to_docker(
+ PATH_TO_CONFIG, config_default, base_image="langchain/langgraph-api:0.2.47"
+ )
+ assert "uv pip install --system " in docker_default
+
+
+def test_config_retain_build_tools():
+ graphs = {"agent": "./graphs/agent.py:graph"}
+ base_config = {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": graphs,
+ }
+ config_true = validate_config(
+ {**copy.deepcopy(base_config), "keep_pkg_tools": True}
+ )
+ docker_true, _ = config_to_docker(
+ PATH_TO_CONFIG, config_true, base_image="langchain/langgraph-api:0.2.47"
+ )
+ assert not any(
+ "/usr/local/lib/python*/site-packages/" + pckg + "*" in docker_true
+ for pckg in _BUILD_TOOLS
+ )
+ assert "RUN pip uninstall -y pip setuptools wheel" not in docker_true
+ config_false = validate_config(
+ {**copy.deepcopy(base_config), "keep_pkg_tools": False}
+ )
+ docker_false, _ = config_to_docker(
+ PATH_TO_CONFIG, config_false, base_image="langchain/langgraph-api:0.2.47"
+ )
+ assert all(
+ "/usr/local/lib/python*/site-packages/" + pckg + "*" in docker_false
+ for pckg in _BUILD_TOOLS
+ )
+ assert "RUN pip uninstall -y pip setuptools wheel" in docker_false
+ config_list = validate_config(
+ {**copy.deepcopy(base_config), "keep_pkg_tools": ["pip", "setuptools"]}
+ )
+ docker_list, _ = config_to_docker(
+ PATH_TO_CONFIG, config_list, base_image="langchain/langgraph-api:0.2.47"
+ )
+ assert all(
+ "/usr/local/lib/python*/site-packages/" + pckg + "*" in docker_list
+ for pckg in ("wheel",)
+ )
+ assert not any(
+ "/usr/local/lib/python*/site-packages/" + pckg + "*" in docker_list
+ for pckg in ("pip", "setuptools")
+ )
+ assert "RUN pip uninstall -y wheel" in docker_list
+ assert "RUN pip uninstall -y pip setuptools" not in docker_list
+
+
+# config_to_compose
+def test_config_to_compose_simple_config():
+ graphs = {"agent": "./agent.py:graph"}
+ # Create a properly indented version of FORMATTED_CLEANUP_LINES for compose files
+ expected_compose_stdin = f"""
+ pull_policy: build
+ build:
+ context: .
+ dockerfile_inline: |
+ FROM langchain/langgraph-api:3.11
+ # -- Adding non-package dependency unit_tests --
+ ADD . /deps/outer-unit_tests/unit_tests
+ RUN set -ex && \\
+ for line in '[project]' \\
+ 'name = "unit_tests"' \\
+ 'version = "0.1"' \\
+ '[tool.setuptools.package-data]' \\
+ '"*" = ["**/*"]' \\
+ '[build-system]' \\
+ 'requires = ["setuptools>=61"]' \\
+ 'build-backend = "setuptools.build_meta"'; do \\
+ echo "$line" >> /deps/outer-unit_tests/pyproject.toml; \\
+ done
+ # -- End of non-package dependency unit_tests --
+ # -- Installing all local dependencies --
+ RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
+ # -- End of local dependencies install --
+ ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
+{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
+ WORKDIR /deps/outer-unit_tests/unit_tests
+ """
+ actual_compose_stdin = config_to_compose(
+ PATH_TO_CONFIG,
+ validate_config({"dependencies": ["."], "graphs": graphs}),
+ "langchain/langgraph-api",
+ )
+ assert (
+ clean_empty_lines(actual_compose_stdin).strip()
+ == expected_compose_stdin.strip()
+ )
+
+
+def test_config_to_compose_env_vars():
+ graphs = {"agent": "./agent.py:graph"}
+ expected_compose_stdin = f""" OPENAI_API_KEY: "key"
+
+ pull_policy: build
+ build:
+ context: .
+ dockerfile_inline: |
+ FROM langchain/langgraph-api-custom:3.11
+ # -- Adding non-package dependency unit_tests --
+ ADD . /deps/outer-unit_tests/unit_tests
+ RUN set -ex && \\
+ for line in '[project]' \\
+ 'name = "unit_tests"' \\
+ 'version = "0.1"' \\
+ '[tool.setuptools.package-data]' \\
+ '"*" = ["**/*"]' \\
+ '[build-system]' \\
+ 'requires = ["setuptools>=61"]' \\
+ 'build-backend = "setuptools.build_meta"'; do \\
+ echo "$line" >> /deps/outer-unit_tests/pyproject.toml; \\
+ done
+ # -- End of non-package dependency unit_tests --
+ # -- Installing all local dependencies --
+ RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
+ # -- End of local dependencies install --
+ ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
+{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
+ WORKDIR /deps/outer-unit_tests/unit_tests
+ """
+ openai_api_key = "key"
+ actual_compose_stdin = config_to_compose(
+ PATH_TO_CONFIG,
+ validate_config(
+ {
+ "dependencies": ["."],
+ "graphs": graphs,
+ "env": {"OPENAI_API_KEY": openai_api_key},
+ }
+ ),
+ "langchain/langgraph-api-custom",
+ )
+ assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
+
+
+def test_config_to_compose_env_file():
+ graphs = {"agent": "./agent.py:graph"}
+ expected_compose_stdin = f"""\
+ env_file: .env
+ pull_policy: build
+ build:
+ context: .
+ dockerfile_inline: |
+ FROM langchain/langgraph-api:3.11
+ # -- Adding non-package dependency unit_tests --
+ ADD . /deps/outer-unit_tests/unit_tests
+ RUN set -ex && \\
+ for line in '[project]' \\
+ 'name = "unit_tests"' \\
+ 'version = "0.1"' \\
+ '[tool.setuptools.package-data]' \\
+ '"*" = ["**/*"]' \\
+ '[build-system]' \\
+ 'requires = ["setuptools>=61"]' \\
+ 'build-backend = "setuptools.build_meta"'; do \\
+ echo "$line" >> /deps/outer-unit_tests/pyproject.toml; \\
+ done
+ # -- End of non-package dependency unit_tests --
+ # -- Installing all local dependencies --
+ RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
+ # -- End of local dependencies install --
+ ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
+{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
+ WORKDIR /deps/outer-unit_tests/unit_tests
+ """
+ actual_compose_stdin = config_to_compose(
+ PATH_TO_CONFIG,
+ validate_config({"dependencies": ["."], "graphs": graphs, "env": ".env"}),
+ "langchain/langgraph-api",
+ )
+ assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
+
+
+def test_config_to_compose_watch():
+ graphs = {"agent": "./agent.py:graph"}
+ expected_compose_stdin = f"""\
+
+ pull_policy: build
+ build:
+ context: .
+ dockerfile_inline: |
+ FROM langchain/langgraph-api:3.11
+ # -- Adding non-package dependency unit_tests --
+ ADD . /deps/outer-unit_tests/unit_tests
+ RUN set -ex && \\
+ for line in '[project]' \\
+ 'name = "unit_tests"' \\
+ 'version = "0.1"' \\
+ '[tool.setuptools.package-data]' \\
+ '"*" = ["**/*"]' \\
+ '[build-system]' \\
+ 'requires = ["setuptools>=61"]' \\
+ 'build-backend = "setuptools.build_meta"'; do \\
+ echo "$line" >> /deps/outer-unit_tests/pyproject.toml; \\
+ done
+ # -- End of non-package dependency unit_tests --
+ # -- Installing all local dependencies --
+ RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
+ # -- End of local dependencies install --
+ ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
+{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
+ WORKDIR /deps/outer-unit_tests/unit_tests
+
+ develop:
+ watch:
+ - path: test_config.json
+ action: rebuild
+ - path: .
+ action: rebuild\
+"""
+ actual_compose_stdin = config_to_compose(
+ PATH_TO_CONFIG,
+ validate_config({"dependencies": ["."], "graphs": graphs}),
+ "langchain/langgraph-api",
+ watch=True,
+ )
+ assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
+
+
+def test_config_to_compose_end_to_end():
+ # test all of the above + langgraph API path
+ graphs = {"agent": "./agent.py:graph"}
+ expected_compose_stdin = f"""\
+ env_file: .env
+ pull_policy: build
+ build:
+ context: .
+ dockerfile_inline: |
+ FROM langchain/langgraph-api:3.11
+ # -- Adding non-package dependency unit_tests --
+ ADD . /deps/outer-unit_tests/unit_tests
+ RUN set -ex && \\
+ for line in '[project]' \\
+ 'name = "unit_tests"' \\
+ 'version = "0.1"' \\
+ '[tool.setuptools.package-data]' \\
+ '"*" = ["**/*"]' \\
+ '[build-system]' \\
+ 'requires = ["setuptools>=61"]' \\
+ 'build-backend = "setuptools.build_meta"'; do \\
+ echo "$line" >> /deps/outer-unit_tests/pyproject.toml; \\
+ done
+ # -- End of non-package dependency unit_tests --
+ # -- Installing all local dependencies --
+ RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
+ # -- End of local dependencies install --
+ ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
+{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
+ WORKDIR /deps/outer-unit_tests/unit_tests
+
+ develop:
+ watch:
+ - path: test_config.json
+ action: rebuild
+ - path: .
+ action: rebuild\
+"""
+ actual_compose_stdin = config_to_compose(
+ PATH_TO_CONFIG,
+ validate_config({"dependencies": ["."], "graphs": graphs, "env": ".env"}),
+ "langchain/langgraph-api",
+ watch=True,
+ )
+ assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
+
+
+def test_docker_tag_image_distro():
+ """Test docker_tag function with different image_distro configurations."""
+
+ # Test 1: Default distro (debian) - no suffix
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ }
+ )
+ tag = docker_tag(config)
+ assert tag == "langchain/langgraph-api:3.11"
+
+ # Test 2: Explicit debian distro - no suffix (same as default)
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "image_distro": "debian",
+ }
+ )
+ tag = docker_tag(config)
+ assert tag == "langchain/langgraph-api:3.11"
+
+ # Test 3: Wolfi distro - should add suffix
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "image_distro": "wolfi",
+ }
+ )
+ tag = docker_tag(config)
+ assert tag == "langchain/langgraph-api:3.11-wolfi"
+
+ # Test 4: Node.js with default distro
+ config = validate_config(
+ {
+ "node_version": "20",
+ "graphs": {"agent": "./agent.js:graph"},
+ }
+ )
+ tag = docker_tag(config)
+ assert tag == "langchain/langgraphjs-api:20"
+
+ # Test 5: Node.js with wolfi distro
+ config = validate_config(
+ {
+ "node_version": "20",
+ "graphs": {"agent": "./agent.js:graph"},
+ "image_distro": "wolfi",
+ }
+ )
+ tag = docker_tag(config)
+ assert tag == "langchain/langgraphjs-api:20-wolfi"
+
+ # Test 6: Custom base image with wolfi
+ config = validate_config(
+ {
+ "python_version": "3.12",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "image_distro": "wolfi",
+ "base_image": "my-registry/custom-image",
+ }
+ )
+ tag = docker_tag(config, base_image="my-registry/custom-image")
+ assert tag == "my-registry/custom-image:3.12-wolfi"
+
+
+def test_docker_tag_multiplatform_with_distro():
+ """Test docker_tag with multiplatform configs and image_distro."""
+
+ # Test 1: Multiplatform (Python + Node) with wolfi
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "node_version": "20",
+ "dependencies": ["."],
+ "graphs": {"python": "./agent.py:graph", "js": "./agent.js:graph"},
+ "image_distro": "wolfi",
+ }
+ )
+ tag = docker_tag(config)
+ # Should default to Python when both are present
+ assert tag == "langchain/langgraph-api:3.11-wolfi"
+
+ # Test 2: Node-only multiplatform with wolfi
+ config = validate_config(
+ {
+ "node_version": "20",
+ "graphs": {"js": "./agent.js:graph"},
+ "image_distro": "wolfi",
+ }
+ )
+ tag = docker_tag(config)
+ assert tag == "langchain/langgraphjs-api:20-wolfi"
+
+
+def test_docker_tag_different_python_versions_with_distro():
+ """Test docker_tag with different Python versions and distros."""
+
+ versions_and_expected = [
+ ("3.11", "langchain/langgraph-api:3.11-wolfi"),
+ ("3.12", "langchain/langgraph-api:3.12-wolfi"),
+ ("3.13", "langchain/langgraph-api:3.13-wolfi"),
+ ]
+
+ for python_version, expected_tag in versions_and_expected:
+ config = validate_config(
+ {
+ "python_version": python_version,
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "image_distro": "wolfi",
+ }
+ )
+ tag = docker_tag(config)
+ assert tag == expected_tag, f"Failed for Python {python_version}"
+
+
+def test_docker_tag_different_node_versions_with_distro():
+ """Test docker_tag with different Node.js versions and distros."""
+
+ versions_and_expected = [
+ ("20", "langchain/langgraphjs-api:20-wolfi"),
+ ("21", "langchain/langgraphjs-api:21-wolfi"),
+ ("22", "langchain/langgraphjs-api:22-wolfi"),
+ ]
+
+ for node_version, expected_tag in versions_and_expected:
+ config = validate_config(
+ {
+ "node_version": node_version,
+ "graphs": {"agent": "./agent.js:graph"},
+ "image_distro": "wolfi",
+ }
+ )
+ tag = docker_tag(config)
+ assert tag == expected_tag, f"Failed for Node.js {node_version}"
+
+
+@pytest.mark.parametrize("in_config", [False, True])
+def test_docker_tag_with_api_version(in_config: bool):
+ """Test docker_tag function with api_version parameter."""
+
+ # Test 1: Python config with api_version and default distro
+ version = "0.2.74"
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "api_version": version if in_config else None,
+ }
+ )
+ tag = docker_tag(config, api_version=version if not in_config else None)
+ assert tag == f"langchain/langgraph-api:{version}-py3.11"
+
+ # Test 2: Python config with api_version and wolfi distro
+ config = validate_config(
+ {
+ "python_version": "3.12",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "image_distro": "wolfi",
+ "api_version": version if in_config else None,
+ }
+ )
+ tag = docker_tag(config, api_version=version if not in_config else None)
+ assert tag == f"langchain/langgraph-api:{version}-py3.12-wolfi"
+
+ # Test 3: Node.js config with api_version and default distro
+ config = validate_config(
+ {
+ "node_version": "20",
+ "graphs": {"agent": "./agent.js:graph"},
+ "api_version": version if in_config else None,
+ }
+ )
+ tag = docker_tag(config, api_version=version if not in_config else None)
+ assert tag == f"langchain/langgraphjs-api:{version}-node20"
+
+ # Test 4: Node.js config with api_version and wolfi distro
+ config = validate_config(
+ {
+ "node_version": "20",
+ "graphs": {"agent": "./agent.js:graph"},
+ "image_distro": "wolfi",
+ "api_version": version if in_config else None,
+ }
+ )
+ tag = docker_tag(config, api_version=version if not in_config else None)
+ assert tag == f"langchain/langgraphjs-api:{version}-node20-wolfi"
+
+ # Test 5: Custom base image with api_version
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "base_image": "my-registry/custom-image",
+ "api_version": version if in_config else None,
+ }
+ )
+ tag = docker_tag(
+ config,
+ base_image="my-registry/custom-image",
+ api_version=version if not in_config else None,
+ )
+ assert tag == f"my-registry/custom-image:{version}-py3.11"
+
+ # Test 6: api_version with different Python versions
+ for python_version in ["3.11", "3.12", "3.13"]:
+ config = validate_config(
+ {
+ "python_version": python_version,
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "api_version": version if in_config else None,
+ }
+ )
+ tag = docker_tag(config, api_version=version if not in_config else None)
+ assert tag == f"langchain/langgraph-api:{version}-py{python_version}"
+
+ # Test 7: Without api_version should work as before
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ }
+ )
+ tag = docker_tag(config)
+ assert tag == "langchain/langgraph-api:3.11"
+
+ # Test 8: api_version with multiplatform config (should default to Python)
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "node_version": "20",
+ "dependencies": ["."],
+ "graphs": {"python": "./agent.py:graph", "js": "./agent.js:graph"},
+ "api_version": version if in_config else None,
+ }
+ )
+ tag = docker_tag(config, api_version=version if not in_config else None)
+ assert tag == f"langchain/langgraph-api:{version}-py3.11"
+
+ # Test 9: api_version with _INTERNAL_docker_tag should ignore api_version
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "_INTERNAL_docker_tag": "internal-tag",
+ }
+ )
+ tag = docker_tag(config, api_version="0.2.74")
+ assert tag == "langchain/langgraph-api:internal-tag"
+
+ # Test 10: api_version with langgraph-server base image should follow special format
+ config = validate_config(
+ {
+ "python_version": "3.11",
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ "api_version": version if in_config else None,
+ }
+ )
+ tag = docker_tag(
+ config,
+ base_image="langchain/langgraph-server",
+ api_version=version if not in_config else None,
+ )
+ assert tag == f"langchain/langgraph-server:{version}-py3.11"
+
+
+def test_config_to_docker_with_api_version():
+ """Test config_to_docker function with api_version parameter."""
+
+ # Test Python config with api_version
+ graphs = {"agent": "./agent.py:graph"}
+ actual_docker_stdin, additional_contexts = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config({"dependencies": ["."], "graphs": graphs}),
+ base_image="langchain/langgraph-api",
+ api_version="0.2.74",
+ )
+
+ # Check that the FROM line uses the api_version
+ lines = actual_docker_stdin.split("\n")
+ from_line = lines[0]
+ assert from_line == "FROM langchain/langgraph-api:0.2.74-py3.11"
+
+ # Test Node.js config with api_version
+ graphs = {"agent": "./agent.js:graph"}
+ actual_docker_stdin, additional_contexts = config_to_docker(
+ PATH_TO_CONFIG,
+ validate_config({"node_version": "20", "graphs": graphs}),
+ base_image="langchain/langgraphjs-api",
+ api_version="0.2.74",
+ )
+
+ # Check that the FROM line uses the api_version
+ lines = actual_docker_stdin.split("\n")
+ from_line = lines[0]
+ assert from_line == "FROM langchain/langgraphjs-api:0.2.74-node20"
+
+
+def test_config_to_compose_with_api_version():
+ """Test config_to_compose function with api_version parameter."""
+
+ # Test Python config with api_version
+ config = validate_config(
+ {
+ "dependencies": ["."],
+ "graphs": {"agent": "./agent.py:graph"},
+ }
+ )
+
+ actual_compose_str = config_to_compose(
+ PATH_TO_CONFIG,
+ config,
+ "langchain/langgraph-api",
+ api_version="0.2.74",
+ )
+
+ # Check that the compose file includes the correct FROM line with api_version
+ assert "FROM langchain/langgraph-api:0.2.74-py3.11" in actual_compose_str
+
+ # Test Node.js config with api_version
+ config = validate_config(
+ {
+ "node_version": "20",
+ "graphs": {"agent": "./agent.js:graph"},
+ }
+ )
+
+ actual_compose_str = config_to_compose(
+ PATH_TO_CONFIG,
+ config,
+ "langchain/langgraphjs-api",
+ api_version="0.2.74",
+ )
+
+ # Check that the compose file includes the correct FROM line with api_version
+ assert "FROM langchain/langgraphjs-api:0.2.74-node20" in actual_compose_str
diff --git a/langgraph/source/libs/cli/tests/unit_tests/test_docker.py b/langgraph/source/libs/cli/tests/unit_tests/test_docker.py
new file mode 100644
index 0000000000000000000000000000000000000000..b16d0df14abbe22e1d816b71a864e37ec8ed589e
--- /dev/null
+++ b/langgraph/source/libs/cli/tests/unit_tests/test_docker.py
@@ -0,0 +1,387 @@
+import pytest
+
+from langgraph_cli.docker import (
+ DEFAULT_POSTGRES_URI,
+ DockerCapabilities,
+ Version,
+ _parse_version,
+ compose,
+)
+from langgraph_cli.util import clean_empty_lines
+
+DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities(
+ version_docker=Version(26, 1, 1),
+ version_compose=Version(2, 27, 0),
+ healthcheck_start_interval=False,
+)
+
+
+def test_compose_with_no_debugger_and_custom_db():
+ port = 8123
+ custom_postgres_uri = "custom_postgres_uri"
+ actual_compose_str = compose(
+ DEFAULT_DOCKER_CAPABILITIES, port=port, postgres_uri=custom_postgres_uri
+ )
+ expected_compose_str = f"""services:
+ langgraph-redis:
+ image: redis:6
+ healthcheck:
+ test: redis-cli ping
+ interval: 5s
+ timeout: 1s
+ retries: 5
+ langgraph-api:
+ ports:
+ - "{port}:8000"
+ depends_on:
+ langgraph-redis:
+ condition: service_healthy
+ environment:
+ REDIS_URI: redis://langgraph-redis:6379
+ POSTGRES_URI: {custom_postgres_uri}"""
+ assert clean_empty_lines(actual_compose_str) == expected_compose_str
+
+
+def test_compose_with_no_debugger_and_custom_db_with_healthcheck():
+ port = 8123
+ custom_postgres_uri = "custom_postgres_uri"
+ actual_compose_str = compose(
+ DEFAULT_DOCKER_CAPABILITIES._replace(healthcheck_start_interval=True),
+ port=port,
+ postgres_uri=custom_postgres_uri,
+ )
+ expected_compose_str = f"""services:
+ langgraph-redis:
+ image: redis:6
+ healthcheck:
+ test: redis-cli ping
+ interval: 5s
+ timeout: 1s
+ retries: 5
+ langgraph-api:
+ ports:
+ - "{port}:8000"
+ depends_on:
+ langgraph-redis:
+ condition: service_healthy
+ environment:
+ REDIS_URI: redis://langgraph-redis:6379
+ POSTGRES_URI: {custom_postgres_uri}
+ healthcheck:
+ test: python /api/healthcheck.py
+ interval: 60s
+ start_interval: 1s
+ start_period: 10s"""
+ assert clean_empty_lines(actual_compose_str) == expected_compose_str
+
+
+def test_compose_with_debugger_and_custom_db():
+ port = 8123
+ custom_postgres_uri = "custom_postgres_uri"
+ actual_compose_str = compose(
+ DEFAULT_DOCKER_CAPABILITIES,
+ port=port,
+ postgres_uri=custom_postgres_uri,
+ )
+ expected_compose_str = f"""services:
+ langgraph-redis:
+ image: redis:6
+ healthcheck:
+ test: redis-cli ping
+ interval: 5s
+ timeout: 1s
+ retries: 5
+ langgraph-api:
+ ports:
+ - "{port}:8000"
+ depends_on:
+ langgraph-redis:
+ condition: service_healthy
+ environment:
+ REDIS_URI: redis://langgraph-redis:6379
+ POSTGRES_URI: {custom_postgres_uri}"""
+ assert clean_empty_lines(actual_compose_str) == expected_compose_str
+
+
+def test_compose_with_debugger_and_default_db():
+ port = 8123
+ actual_compose_str = compose(DEFAULT_DOCKER_CAPABILITIES, port=port)
+ expected_compose_str = f"""volumes:
+ langgraph-data:
+ driver: local
+services:
+ langgraph-redis:
+ image: redis:6
+ healthcheck:
+ test: redis-cli ping
+ interval: 5s
+ timeout: 1s
+ retries: 5
+ langgraph-postgres:
+ image: pgvector/pgvector:pg16
+ ports:
+ - "5433:5432"
+ environment:
+ POSTGRES_DB: postgres
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ command:
+ - postgres
+ - -c
+ - shared_preload_libraries=vector
+ volumes:
+ - langgraph-data:/var/lib/postgresql/data
+ healthcheck:
+ test: pg_isready -U postgres
+ start_period: 10s
+ timeout: 1s
+ retries: 5
+ interval: 5s
+ langgraph-api:
+ ports:
+ - "{port}:8000"
+ depends_on:
+ langgraph-redis:
+ condition: service_healthy
+ langgraph-postgres:
+ condition: service_healthy
+ environment:
+ REDIS_URI: redis://langgraph-redis:6379
+ POSTGRES_URI: {DEFAULT_POSTGRES_URI}"""
+ assert clean_empty_lines(actual_compose_str) == expected_compose_str
+
+
+def test_compose_with_api_version():
+ """Test compose function with api_version parameter."""
+ port = 8123
+ api_version = "0.2.74"
+
+ actual_compose_str = compose(
+ DEFAULT_DOCKER_CAPABILITIES, port=port, api_version=api_version
+ )
+
+ # The compose function should generate a compose file that doesn't directly
+ # reference the api_version, since it's handled in the docker tag creation
+ # when building the image. The compose function mainly sets up services.
+ expected_compose_str = f"""volumes:
+ langgraph-data:
+ driver: local
+services:
+ langgraph-redis:
+ image: redis:6
+ healthcheck:
+ test: redis-cli ping
+ interval: 5s
+ timeout: 1s
+ retries: 5
+ langgraph-postgres:
+ image: pgvector/pgvector:pg16
+ ports:
+ - "5433:5432"
+ environment:
+ POSTGRES_DB: postgres
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ command:
+ - postgres
+ - -c
+ - shared_preload_libraries=vector
+ volumes:
+ - langgraph-data:/var/lib/postgresql/data
+ healthcheck:
+ test: pg_isready -U postgres
+ start_period: 10s
+ timeout: 1s
+ retries: 5
+ interval: 5s
+ langgraph-api:
+ ports:
+ - "{port}:8000"
+ depends_on:
+ langgraph-redis:
+ condition: service_healthy
+ langgraph-postgres:
+ condition: service_healthy
+ environment:
+ REDIS_URI: redis://langgraph-redis:6379
+ POSTGRES_URI: {DEFAULT_POSTGRES_URI}"""
+ assert clean_empty_lines(actual_compose_str) == expected_compose_str
+
+
+def test_compose_with_api_version_and_base_image():
+ """Test compose function with both api_version and base_image parameters."""
+ port = 8123
+ api_version = "1.0.0"
+ base_image = "my-registry/custom-api"
+
+ actual_compose_str = compose(
+ DEFAULT_DOCKER_CAPABILITIES,
+ port=port,
+ api_version=api_version,
+ base_image=base_image,
+ )
+
+ # Similar to the previous test - the compose function doesn't directly embed
+ # the api_version or base_image into the compose file since those are handled
+ # during the docker build process
+ expected_compose_str = f"""volumes:
+ langgraph-data:
+ driver: local
+services:
+ langgraph-redis:
+ image: redis:6
+ healthcheck:
+ test: redis-cli ping
+ interval: 5s
+ timeout: 1s
+ retries: 5
+ langgraph-postgres:
+ image: pgvector/pgvector:pg16
+ ports:
+ - "5433:5432"
+ environment:
+ POSTGRES_DB: postgres
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ command:
+ - postgres
+ - -c
+ - shared_preload_libraries=vector
+ volumes:
+ - langgraph-data:/var/lib/postgresql/data
+ healthcheck:
+ test: pg_isready -U postgres
+ start_period: 10s
+ timeout: 1s
+ retries: 5
+ interval: 5s
+ langgraph-api:
+ ports:
+ - "{port}:8000"
+ depends_on:
+ langgraph-redis:
+ condition: service_healthy
+ langgraph-postgres:
+ condition: service_healthy
+ environment:
+ REDIS_URI: redis://langgraph-redis:6379
+ POSTGRES_URI: {DEFAULT_POSTGRES_URI}"""
+ assert clean_empty_lines(actual_compose_str) == expected_compose_str
+
+
+def test_compose_with_api_version_and_custom_postgres():
+ """Test compose function with api_version and custom postgres URI."""
+ port = 8123
+ api_version = "0.2.74"
+ custom_postgres_uri = "postgresql://user:pass@external-db:5432/mydb"
+
+ actual_compose_str = compose(
+ DEFAULT_DOCKER_CAPABILITIES,
+ port=port,
+ api_version=api_version,
+ postgres_uri=custom_postgres_uri,
+ )
+
+ expected_compose_str = f"""services:
+ langgraph-redis:
+ image: redis:6
+ healthcheck:
+ test: redis-cli ping
+ interval: 5s
+ timeout: 1s
+ retries: 5
+ langgraph-api:
+ ports:
+ - "{port}:8000"
+ depends_on:
+ langgraph-redis:
+ condition: service_healthy
+ environment:
+ REDIS_URI: redis://langgraph-redis:6379
+ POSTGRES_URI: {custom_postgres_uri}"""
+ assert clean_empty_lines(actual_compose_str) == expected_compose_str
+
+
+def test_compose_with_api_version_and_debugger():
+ """Test compose function with api_version and debugger port."""
+ port = 8123
+ debugger_port = 8001
+ api_version = "0.2.74"
+
+ actual_compose_str = compose(
+ DEFAULT_DOCKER_CAPABILITIES,
+ port=port,
+ api_version=api_version,
+ debugger_port=debugger_port,
+ )
+
+ expected_compose_str = f"""volumes:
+ langgraph-data:
+ driver: local
+services:
+ langgraph-redis:
+ image: redis:6
+ healthcheck:
+ test: redis-cli ping
+ interval: 5s
+ timeout: 1s
+ retries: 5
+ langgraph-postgres:
+ image: pgvector/pgvector:pg16
+ ports:
+ - "5433:5432"
+ environment:
+ POSTGRES_DB: postgres
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ command:
+ - postgres
+ - -c
+ - shared_preload_libraries=vector
+ volumes:
+ - langgraph-data:/var/lib/postgresql/data
+ healthcheck:
+ test: pg_isready -U postgres
+ start_period: 10s
+ timeout: 1s
+ retries: 5
+ interval: 5s
+ langgraph-debugger:
+ image: langchain/langgraph-debugger
+ restart: on-failure
+ depends_on:
+ langgraph-postgres:
+ condition: service_healthy
+ ports:
+ - "{debugger_port}:3968"
+ langgraph-api:
+ ports:
+ - "{port}:8000"
+ depends_on:
+ langgraph-redis:
+ condition: service_healthy
+ langgraph-postgres:
+ condition: service_healthy
+ environment:
+ REDIS_URI: redis://langgraph-redis:6379
+ POSTGRES_URI: {DEFAULT_POSTGRES_URI}"""
+ assert clean_empty_lines(actual_compose_str) == expected_compose_str
+
+
+@pytest.mark.parametrize(
+ "input_str,expected",
+ [
+ ("1.2.3", Version(1, 2, 3)),
+ ("v1.2.3", Version(1, 2, 3)),
+ ("1.2.3-alpha", Version(1, 2, 3)),
+ ("1.2.3+1", Version(1, 2, 3)),
+ ("1.2.3-alpha+build", Version(1, 2, 3)),
+ ("1.2", Version(1, 2, 0)),
+ ("1", Version(1, 0, 0)),
+ ("v28.1.1+1", Version(28, 1, 1)),
+ ("2.0.0-beta.1+exp.sha.5114f85", Version(2, 0, 0)),
+ ("v3.4.5-rc1+build.123", Version(3, 4, 5)),
+ ],
+)
+def test_parse_version_w_edge_cases(input_str, expected):
+ assert _parse_version(input_str) == expected
diff --git a/langgraph/source/libs/cli/tests/unit_tests/test_util.py b/langgraph/source/libs/cli/tests/unit_tests/test_util.py
new file mode 100644
index 0000000000000000000000000000000000000000..f4b4399590e9374fc46ed0ad2f382a90180dc3d0
--- /dev/null
+++ b/langgraph/source/libs/cli/tests/unit_tests/test_util.py
@@ -0,0 +1,188 @@
+from unittest.mock import patch
+
+from langgraph_cli.util import clean_empty_lines, warn_non_wolfi_distro
+
+
+def test_clean_empty_lines():
+ """Test clean_empty_lines function."""
+ # Test with empty lines
+ input_str = "line1\n\nline2\n\nline3"
+ result = clean_empty_lines(input_str)
+ assert result == "line1\nline2\nline3"
+
+ # Test with no empty lines
+ input_str = "line1\nline2\nline3"
+ result = clean_empty_lines(input_str)
+ assert result == "line1\nline2\nline3"
+
+ # Test with only empty lines
+ input_str = "\n\n\n"
+ result = clean_empty_lines(input_str)
+ assert result == ""
+
+ # Test empty string
+ input_str = ""
+ result = clean_empty_lines(input_str)
+ assert result == ""
+
+
+def test_warn_non_wolfi_distro_with_debian(capsys):
+ """Test that warning is shown when image_distro is 'debian'."""
+ config = {"image_distro": "debian"}
+
+ warn_non_wolfi_distro(config)
+
+ captured = capsys.readouterr()
+ assert (
+ "⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security."
+ in captured.out
+ )
+ assert (
+ "Wolfi is a security-oriented, minimal Linux distribution designed for containers."
+ in captured.out
+ )
+ assert (
+ 'To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.'
+ in captured.out
+ )
+
+
+def test_warn_non_wolfi_distro_with_default_debian(capsys):
+ """Test that warning is shown when image_distro is missing (defaults to debian)."""
+ config = {} # No image_distro key, should default to debian
+
+ warn_non_wolfi_distro(config)
+
+ captured = capsys.readouterr()
+ assert (
+ "⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security."
+ in captured.out
+ )
+ assert (
+ "Wolfi is a security-oriented, minimal Linux distribution designed for containers."
+ in captured.out
+ )
+ assert (
+ 'To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.'
+ in captured.out
+ )
+
+
+def test_warn_non_wolfi_distro_with_wolfi(capsys):
+ """Test that no warning is shown when image_distro is 'wolfi'."""
+ config = {"image_distro": "wolfi"}
+
+ warn_non_wolfi_distro(config)
+
+ captured = capsys.readouterr()
+ assert captured.out == "" # No output should be generated
+
+
+def test_warn_non_wolfi_distro_with_other_distro(capsys):
+ """Test that warning is shown when image_distro is something other than 'wolfi'."""
+ config = {"image_distro": "ubuntu"}
+
+ warn_non_wolfi_distro(config)
+
+ captured = capsys.readouterr()
+ assert (
+ "⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security."
+ in captured.out
+ )
+ assert (
+ "Wolfi is a security-oriented, minimal Linux distribution designed for containers."
+ in captured.out
+ )
+ assert (
+ 'To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.'
+ in captured.out
+ )
+
+
+def test_warn_non_wolfi_distro_output_formatting():
+ """Test that the warning output is properly formatted with colors and empty line."""
+ config = {"image_distro": "debian"}
+
+ with patch("click.secho") as mock_secho:
+ warn_non_wolfi_distro(config)
+
+ # Verify click.secho was called with the correct parameters
+ expected_calls = [
+ (
+ (
+ "⚠️ Security Recommendation: Consider switching to Wolfi Linux for enhanced security.",
+ ),
+ {"fg": "yellow", "bold": True},
+ ),
+ (
+ (
+ " Wolfi is a security-oriented, minimal Linux distribution designed for containers.",
+ ),
+ {"fg": "yellow"},
+ ),
+ (
+ (
+ ' To switch, add \'"image_distro": "wolfi"\' to your langgraph.json config file.',
+ ),
+ {"fg": "yellow"},
+ ),
+ (
+ ("",), # Empty line
+ {},
+ ),
+ ]
+
+ assert mock_secho.call_count == 4
+ for i, (expected_args, expected_kwargs) in enumerate(expected_calls):
+ actual_call = mock_secho.call_args_list[i]
+ assert actual_call.args == expected_args
+ assert actual_call.kwargs == expected_kwargs
+
+
+def test_warn_non_wolfi_distro_various_configs(capsys):
+ """Test warn_non_wolfi_distro with various config scenarios."""
+ test_cases = [
+ # (config, should_warn, description)
+ ({"image_distro": "debian"}, True, "explicit debian"),
+ ({"image_distro": "wolfi"}, False, "explicit wolfi"),
+ ({}, True, "missing image_distro (defaults to debian)"),
+ ({"image_distro": "alpine"}, True, "other distro"),
+ ({"image_distro": "ubuntu"}, True, "ubuntu distro"),
+ ({"other_config": "value"}, True, "unrelated config keys"),
+ ]
+
+ for config, should_warn, description in test_cases:
+ # Clear any previous output
+ capsys.readouterr()
+
+ warn_non_wolfi_distro(config)
+
+ captured = capsys.readouterr()
+ if should_warn:
+ assert "⚠️ Security Recommendation" in captured.out, (
+ f"Should warn for {description}"
+ )
+ assert "Wolfi" in captured.out, f"Should mention Wolfi for {description}"
+ else:
+ assert captured.out == "", f"Should not warn for {description}"
+
+
+def test_warn_non_wolfi_distro_return_value():
+ """Test that warn_non_wolfi_distro returns None."""
+ config = {"image_distro": "debian"}
+ result = warn_non_wolfi_distro(config)
+ assert result is None
+
+ config = {"image_distro": "wolfi"}
+ result = warn_non_wolfi_distro(config)
+ assert result is None
+
+
+def test_warn_non_wolfi_distro_does_not_modify_config():
+ """Test that warn_non_wolfi_distro does not modify the input config."""
+ original_config = {"image_distro": "debian", "other_key": "value"}
+ config_copy = original_config.copy()
+
+ warn_non_wolfi_distro(config_copy)
+
+ assert config_copy == original_config # Config should remain unchanged
diff --git a/langgraph/source/libs/cli/uv.lock b/langgraph/source/libs/cli/uv.lock
new file mode 100644
index 0000000000000000000000000000000000000000..c63d1690ec539a0fba18a351eda96e9792806968
--- /dev/null
+++ b/langgraph/source/libs/cli/uv.lock
@@ -0,0 +1,2635 @@
+version = 1
+revision = 3
+requires-python = ">=3.10"
+resolution-markers = [
+ "python_full_version >= '3.11'",
+ "python_full_version < '3.11'",
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.12.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
+]
+
+[[package]]
+name = "backports-asyncio-runner"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" },
+]
+
+[[package]]
+name = "backports-tarfile"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" },
+]
+
+[[package]]
+name = "backports-zstd"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/36a5182ce1d8ef9ef32bff69037bd28b389bbdb66338f8069e61da7028cb/backports_zstd-1.3.0.tar.gz", hash = "sha256:e8b2d68e2812f5c9970cabc5e21da8b409b5ed04e79b4585dbffa33e9b45ebe2", size = 997138, upload-time = "2025-12-29T17:28:06.143Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/70/766f6ebbb9db2ed75951f0a671ee15931dc69278c84d9f09b08dd6b67c3e/backports_zstd-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a2db17a6d9bf6b4dc223b3f6414aa9db6d1afe9de9bff61d582c2934ca456a0", size = 435664, upload-time = "2025-12-29T17:25:29.201Z" },
+ { url = "https://files.pythonhosted.org/packages/55/f8/7b3fad9c6ee5ff3bcd7c941586675007330197ff4a388f01c73198ecc8bb/backports_zstd-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7f16b98ba81780a9517ce6c493e1aea9b7d72de2b1efa08375136c270e1ecba", size = 362060, upload-time = "2025-12-29T17:25:30.94Z" },
+ { url = "https://files.pythonhosted.org/packages/68/9e/cad0f508ed7c3fbd07398f22b5bf25aa0523fcf56c84c3def642909e80ae/backports_zstd-1.3.0-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:1124a169a647671ccb4654a0ef1d0b42d6735c45ce3d0adf609df22fb1f099db", size = 505958, upload-time = "2025-12-29T17:25:32.694Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/dc/96dc55c043b0d86e53ae9608b496196936244c1ecf7e95cdf66d0dbc0f23/backports_zstd-1.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8410fda08b36202d01ab4503f6787c763898888cb1a48c19fce94711563d3ee3", size = 475571, upload-time = "2025-12-29T17:25:33.9Z" },
+ { url = "https://files.pythonhosted.org/packages/20/48/d9c8c8c2a5ac57fc5697f1945254af31407b0c5f80335a175a7c215b4118/backports_zstd-1.3.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab139d1fc0e91a697e82fa834e6404098802f11b6035607174776173ded9a2cc", size = 581199, upload-time = "2025-12-29T17:25:35.566Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/ca/7fe70d2d39ed39e26a6c6f6c1dd229f1ab889500d5c90b17527702b1a21e/backports_zstd-1.3.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6f3115d203f387f77c23b5461fb6678d282d4f276f9f39298ad242b00120afc7", size = 640846, upload-time = "2025-12-29T17:25:36.86Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/d8/5b8580469e70b72402212885bf19b9d31eaf23549b602e0c294edf380e25/backports_zstd-1.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:116f65cce84e215dfac0414924b051faf8d29dc7188cf3944dd1e5be8dd15a32", size = 491061, upload-time = "2025-12-29T17:25:38.721Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/dd/17a752263fccd1ba24184b7e89c14cd31553d512e2e5b065f38e63a0ba86/backports_zstd-1.3.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04def169e4a9ae291298124da4e097c6d6545d0e93164f934b716da04d24630a", size = 565071, upload-time = "2025-12-29T17:25:40.372Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/81/df23d3fe664b2497ab2ec01dc012cb9304e7d568c67f50b1b324fb2d8cbb/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:481b586291ef02a250f03d4c31a37c9881e5e93556568abbd20ca1ad720d443f", size = 481518, upload-time = "2025-12-29T17:25:41.925Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/cd/e50dd85fde890c5d79e1ed5dc241f1c45f87b6c12571fdb60add57f2ee66/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0290979eea67f7275fa42d5859cc5bea94f2c08cca6bc36396673476773d2bad", size = 509464, upload-time = "2025-12-29T17:25:43.844Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/bb/e429156e4b834837fe78b4f32ed512491aea39415444420c79ccd3aa0526/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:01c699d8c803dc9f9c9d6ede21b75ec99f45c3b411821011692befca538928cb", size = 585563, upload-time = "2025-12-29T17:25:45.038Z" },
+ { url = "https://files.pythonhosted.org/packages/95/c0/1a0d245325827242aefe76f4f3477ec183b996b8db5105698564f8303481/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:2c662912cfc1a5ebd1d2162ac651549d58bd3c97a8096130ec13c703fca355f2", size = 562889, upload-time = "2025-12-29T17:25:46.576Z" },
+ { url = "https://files.pythonhosted.org/packages/93/42/126b2bc7540a15452c3ebdf190ebfea8a8644e29b22f4e10e2a6aa2389e4/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3180c8eb085396928e9946167e610aa625922b82c3e2263c5f17000556370168", size = 631423, upload-time = "2025-12-29T17:25:47.81Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/32/018e49657411582569032b7d1bb5d62e514aad8b44952de740ec6250588d/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5b9a8c75a294e7ffa18fc8425a763facc366435a8b442e4dffdc19fa9499a22c", size = 495122, upload-time = "2025-12-29T17:25:49.377Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/9e/cdd1d2e1d3612bb90d9cf9b23bea06f2155cdafccd8b6f28a1c4d7750004/backports_zstd-1.3.0-cp310-cp310-win32.whl", hash = "sha256:845defdb172385f17123d92a00d2e952d341e9ae310bfa2410c292bf03846034", size = 288573, upload-time = "2025-12-29T17:25:51.167Z" },
+ { url = "https://files.pythonhosted.org/packages/55/7c/2e9c80f08375bd14262cefa69297a926134f517c9955c0795eec5e1d470e/backports_zstd-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:43a9fea6299c801da85221e387b32d90a9ad7c62aa2a34edf525359ce5ad8f3a", size = 313506, upload-time = "2025-12-29T17:25:52.778Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/5d/fa67e8174f54db44eb33498abb7f98bea4f2329e873b225391bda0113a5e/backports_zstd-1.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:df8473cb117e1316e6c6101f2724e025bd8f50af2dc009d0001c0aabfb5eb57c", size = 288688, upload-time = "2025-12-29T17:25:54.012Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/28/ed31a0e35feb4538a996348362051b52912d50f00d25c2d388eccef9242c/backports_zstd-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:249f90b39d3741c48620021a968b35f268ca70e35f555abeea9ff95a451f35f9", size = 435660, upload-time = "2025-12-29T17:25:55.207Z" },
+ { url = "https://files.pythonhosted.org/packages/00/0d/3db362169d80442adda9dd563c4f0bb10091c8c1c9a158037f4ecd53988e/backports_zstd-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b0e71e83e46154a9d3ced6d4de9a2fea8207ee1e4832aeecf364dc125eda305c", size = 362056, upload-time = "2025-12-29T17:25:56.729Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/00/b67ba053a7d6f6dbe2f8a704b7d3a5e01b1d2e2e8edbc9b634f2702ef73c/backports_zstd-1.3.0-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:cbc6193acd21f96760c94dd71bf32b161223e8503f5277acb0a5ab54e5598957", size = 505957, upload-time = "2025-12-29T17:25:57.941Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/3e/2667c0ddb53ddf28667e330bf9fe92e8e17705a481c9b698e283120565f7/backports_zstd-1.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1df583adc0ae84a8d13d7139f42eade6d90182b1dd3e0d28f7df3c564b9fd55d", size = 475569, upload-time = "2025-12-29T17:25:59.075Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/86/4052473217bd954ccdffda5f7264a0e99e7c4ecf70c0f729845c6a45fc5a/backports_zstd-1.3.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d833fc23aa3cc2e05aeffc7cfadd87b796654ad3a7fb214555cda3f1db2d4dc2", size = 581196, upload-time = "2025-12-29T17:26:00.508Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/bd/064f6fdb61db3d2c473159ebc844243e650dc032de0f8208443a00127925/backports_zstd-1.3.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:142178fe981061f1d2a57c5348f2cd31a3b6397a35593e7a17dbda817b793a7f", size = 640888, upload-time = "2025-12-29T17:26:02.134Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/09/0822403f40932a165a4f1df289d41653683019e4fd7a86b63ed20e9b6177/backports_zstd-1.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5eed0a09a163f3a8125a857cb031be87ed052e4a47bc75085ed7fca786e9bb5b", size = 491100, upload-time = "2025-12-29T17:26:03.418Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/a3/f5ac28d74039b7e182a780809dc66b9dbfc893186f5d5444340bba135389/backports_zstd-1.3.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:60aa483fef5843749e993dde01229e5eedebca8c283023d27d6bf6800d1d4ce3", size = 565071, upload-time = "2025-12-29T17:26:05.022Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/ac/50209aeb92257a642ee987afa1e61d5b6731ab6bf0bff70905856e5aede6/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ea0886c1b619773544546e243ed73f6d6c2b1ae3c00c904ccc9903a352d731e1", size = 481519, upload-time = "2025-12-29T17:26:06.255Z" },
+ { url = "https://files.pythonhosted.org/packages/08/1f/b06f64199fb4b2e9437cedbf96d0155ca08aeec35fe81d41065acd44762e/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5e137657c830a5ce99be40a1d713eb1d246bae488ada28ff0666ac4387aebdd5", size = 509465, upload-time = "2025-12-29T17:26:07.602Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/37/2c365196e61c8fffbbc930ffd69f1ada7aa1c7210857b3e565031c787ac6/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:94048c8089755e482e4b34608029cf1142523a625873c272be2b1c9253871a72", size = 585552, upload-time = "2025-12-29T17:26:08.911Z" },
+ { url = "https://files.pythonhosted.org/packages/93/8d/c2c4f448bb6b6c9df17410eaedce415e8db0eb25b60d09a3d22a98294d09/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:d339c1ec40485e97e600eb9a285fb13169dbf44c5094b945788a62f38b96e533", size = 562893, upload-time = "2025-12-29T17:26:10.566Z" },
+ { url = "https://files.pythonhosted.org/packages/74/e8/2110d4d39115130f7514cbbcec673a885f4052bb68d15e41bc96a7558856/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8aeee9210c54cf8bf83f4d263a6d0d6e7a0298aeb5a14a0a95e90487c5c3157c", size = 631462, upload-time = "2025-12-29T17:26:11.99Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/a8/d64b59ae0714fdace14e43873f794eff93613e35e3e85eead33a4f44cd80/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba7114a3099e5ea05cbb46568bd0e08bca2ca11e12c6a7b563a24b86b2b4a67f", size = 495125, upload-time = "2025-12-29T17:26:13.218Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/d8/bcff0a091fcf27172c57ae463e49d8dec6dc31e01d7e7bf1ae3aad9c3566/backports_zstd-1.3.0-cp311-cp311-win32.whl", hash = "sha256:08dfdfb85da5915383bfae680b6ac10ab5769ab22e690f9a854320720011ae8e", size = 288664, upload-time = "2025-12-29T17:26:14.791Z" },
+ { url = "https://files.pythonhosted.org/packages/28/1a/379061e2abf8c3150ad51c1baab9ac723e01cf7538860a6a74c48f8b73ee/backports_zstd-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8aac2e7cdcc8f310c16f98a0062b48d0a081dbb82862794f4f4f5bdafde30a4", size = 313633, upload-time = "2025-12-29T17:26:16.31Z" },
+ { url = "https://files.pythonhosted.org/packages/35/e7/eca40858883029fc716660106069b23253e2ec5fd34e86b4101c8cfe864b/backports_zstd-1.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:440ef1be06e82dc0d69dbb57177f2ce98bbd2151013ee7e551e2f2b54caa6120", size = 288814, upload-time = "2025-12-29T17:26:17.571Z" },
+ { url = "https://files.pythonhosted.org/packages/72/d4/356da49d3053f4bc50e71a8535631b57bc9ca4e8c6d2442e073e0ab41c44/backports_zstd-1.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f4a292e357f3046d18766ce06d990ccbab97411708d3acb934e63529c2ea7786", size = 435972, upload-time = "2025-12-29T17:26:18.752Z" },
+ { url = "https://files.pythonhosted.org/packages/30/8f/dbe389e60c7e47af488520f31a4aa14028d66da5bf3c60d3044b571eb906/backports_zstd-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb4c386f38323698991b38edcc9c091d46d4713f5df02a3b5c80a28b40e289ea", size = 362124, upload-time = "2025-12-29T17:26:19.995Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4b/173beafc99e99e7276ce008ef060b704471e75124c826bc5e2092815da37/backports_zstd-1.3.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f52523d2bdada29e653261abdc9cfcecd9e5500d305708b7e37caddb24909d4e", size = 506378, upload-time = "2025-12-29T17:26:21.855Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c8/3f12a411d9a99d262cdb37b521025eecc2aa7e4a93277be3f4f4889adb74/backports_zstd-1.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3321d00beaacbd647252a7f581c1e1cdbdbda2407f2addce4bfb10e8e404b7c7", size = 476201, upload-time = "2025-12-29T17:26:23.047Z" },
+ { url = "https://files.pythonhosted.org/packages/43/dc/73c090e4a2d5671422512e1b6d276ca6ea0cc0c45ec4634789106adc0d66/backports_zstd-1.3.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:88f94d238ef36c639c0ae17cf41054ce103da9c4d399c6a778ce82690d9f4919", size = 581659, upload-time = "2025-12-29T17:26:24.189Z" },
+ { url = "https://files.pythonhosted.org/packages/08/4f/11bfcef534aa2bf3f476f52130217b45337f334d8a287edb2e06744a6515/backports_zstd-1.3.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97d8c78fe20c7442c810adccfd5e3ea6a4e6f4f1fa4c73da2bc083260ebead17", size = 640388, upload-time = "2025-12-29T17:26:25.47Z" },
+ { url = "https://files.pythonhosted.org/packages/71/17/8faea426d4f49b63238bdfd9f211a9f01c862efe0d756d3abeb84265a4e2/backports_zstd-1.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eefda80c3dbfbd924f1c317e7b0543d39304ee645583cb58bae29e19f42948ed", size = 494173, upload-time = "2025-12-29T17:26:26.736Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/9d/901f19ac90f3cd999bdcfb6edb4d7b4dc383dfba537f06f533fc9ac4777b/backports_zstd-1.3.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ab5d3b5a54a674f4f6367bb9e0914063f22cd102323876135e9cc7a8f14f17e", size = 568628, upload-time = "2025-12-29T17:26:28.12Z" },
+ { url = "https://files.pythonhosted.org/packages/60/39/4d29788590c2465a570c2fae49dbff05741d1f0c8e4a0fb2c1c310f31804/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7558fb0e8c8197c59a5f80c56bf8f56c3690c45fd62f14e9e2081661556e3e64", size = 482233, upload-time = "2025-12-29T17:26:29.399Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/4b/24c7c9e8ef384b19d515a7b1644a500ceb3da3baeff6d579687da1a0f62b/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:27744870e38f017159b9c0241ea51562f94c7fefcfa4c5190fb3ec4a65a7fc63", size = 509806, upload-time = "2025-12-29T17:26:30.605Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/7e/7ba1aeecf0b5859f1855c0e661b4559566b64000f0627698ebd9e83f2138/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b099750755bb74c280827c7d68de621da0f245189082ab48ff91bda0ec2db9df", size = 586037, upload-time = "2025-12-29T17:26:32.201Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/1a/18f0402b36b9cfb0aea010b5df900cfd42c214f37493561dba3abac90c4e/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5434e86f2836d453ae3e19a2711449683b7e21e107686838d12a255ad256ca99", size = 566220, upload-time = "2025-12-29T17:26:33.5Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/d9/44c098ab31b948bbfd909ec4ae08e1e44c5025a2d846f62991a62ab3ebea/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:407e451f64e2f357c9218f5be4e372bb6102d7ae88582d415262a9d0a4f9b625", size = 630847, upload-time = "2025-12-29T17:26:35.273Z" },
+ { url = "https://files.pythonhosted.org/packages/30/33/e74cb2cfb162d2e9e00dad8bcdf53118ca7786cfd467925d6864732f79cc/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58a071f3c198c781b2df801070290b7174e3ff61875454e9df93ab7ea9ea832b", size = 498665, upload-time = "2025-12-29T17:26:37.123Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/a9/67a24007c333ed22736d5cd79f1aa1d7209f09be772ff82a8fd724c1978e/backports_zstd-1.3.0-cp312-cp312-win32.whl", hash = "sha256:21a9a542ccc7958ddb51ae6e46d8ed25d585b54d0d52aaa1c8da431ea158046a", size = 288809, upload-time = "2025-12-29T17:26:38.373Z" },
+ { url = "https://files.pythonhosted.org/packages/42/24/34b816118ea913debb2ea23e71ffd0fb2e2ac738064c4ac32e3fb62c18bb/backports_zstd-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:89ea8281821123b071a06b30b80da8e4d8a2b40a4f57315a19850337a21297ac", size = 313815, upload-time = "2025-12-29T17:26:39.665Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/2f/babd02c9fc4ca35376ada7c291193a208165c7be2455f0f98bc1e1243f31/backports_zstd-1.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:f6843ecb181480e423b02f60fe29e393cbc31a95fb532acdf0d3a2c87bd50ce3", size = 288927, upload-time = "2025-12-29T17:26:40.923Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/7d/53e8da5950cdfc5e8fe23efd5165ce2f4fed5222f9a3292e0cdb03dd8c0d/backports_zstd-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e86e03e3661900955f01afed6c59cae9baa63574e3b66896d99b7de97eaffce9", size = 435463, upload-time = "2025-12-29T17:26:42.152Z" },
+ { url = "https://files.pythonhosted.org/packages/da/78/f98e53870f7404071a41e3d04f2ff514302eeeb3279d931d02b220f437aa/backports_zstd-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:41974dcacc9824c1effe1c8d2f9d762bcf47d265ca4581a3c63321c7b06c61f0", size = 361740, upload-time = "2025-12-29T17:26:43.377Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/ed/2c64706205a944c9c346d95c17f632d4e3468db3ce60efb6f5caa7c0dcae/backports_zstd-1.3.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:3090a97738d6ce9545d3ca5446df43370928092a962cbc0153e5445a947e98ed", size = 505651, upload-time = "2025-12-29T17:26:44.495Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/7b/22998f691dc6e0c7e6fa81d611eb4b1f6a72fb27327f322366d4a7ca8fb3/backports_zstd-1.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddc874638abf03ea1ff3b0525b4a26a8d0adf7cb46a448c3449f08e4abc276b3", size = 475859, upload-time = "2025-12-29T17:26:45.722Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/78/0cde898339a339530e5f932634872d2d64549969535447a48d3b98959e11/backports_zstd-1.3.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:db609e57b8ed88b3472930c87e93c08a4bbd5ffeb94608cd9c7c6f0ac0e166c6", size = 581339, upload-time = "2025-12-29T17:26:46.93Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/1d/e0973e0eebe678c12c146473af2c54cda8a3e63b179785ca1a20727ad69c/backports_zstd-1.3.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5f13033a3dd95f323c067199f2e61b4589a7880188ef4ef356c7ffbdb78a9f11", size = 642182, upload-time = "2025-12-29T17:26:48.545Z" },
+ { url = "https://files.pythonhosted.org/packages/82/a2/ac67e79e137eb98aead66c7162bafe3cffcb82ef9cdeb6367ec18d88fbce/backports_zstd-1.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c4c7bcda5619a754726e7f5b391827f5efbe4bed8e62e9ec7490d42bff18aa6", size = 490807, upload-time = "2025-12-29T17:26:49.789Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/e9/3514b1d065801ae7dce05246e9389003ed8fb1d7c3d71f85aa07a80f41e6/backports_zstd-1.3.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:884a94c40f27affe986f394f219a4fd3cbbd08e1cff2e028d29d467574cd266e", size = 566103, upload-time = "2025-12-29T17:26:51.062Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/03/10ddb54cbf032e5fe390c0776d3392611b1fc772d6c3cb5a9bcdff4f915f/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497f5765126f11a5b3fd8fedfdae0166d1dd867e7179b8148370a3313d047197", size = 481614, upload-time = "2025-12-29T17:26:52.255Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/13/21efa7f94c41447f43aee1563b05fc540a235e61bce4597754f6c11c2e97/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a6ff6769948bb29bba07e1c2e8582d5a9765192a366108e42d6581a458475881", size = 509207, upload-time = "2025-12-29T17:26:53.496Z" },
+ { url = "https://files.pythonhosted.org/packages/de/e7/12da9256d9e49e71030f0ff75e9f7c258e76091a4eaf5b5f414409be6a57/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1623e5bff1acd9c8ef90d24fc548110f20df2d14432bfe5de59e76fc036824ef", size = 585765, upload-time = "2025-12-29T17:26:54.99Z" },
+ { url = "https://files.pythonhosted.org/packages/24/bf/59ca9cb4e7be1e59331bb792e8ef1331828efe596b1a2f8cbbc4e3f70d75/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:622c28306dcc429c8f2057fc4421d5722b1f22968d299025b35d71b50cfd4e03", size = 563852, upload-time = "2025-12-29T17:26:56.371Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/ee/5a3eaed9a73bdf2c35dc0c7adc0616a99588e0de28f5ab52f3e0caaaa96f/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09a2785e410ed2e812cb39b684ef5eb55083a5897bfd0e6f5de3bbd2c6345f70", size = 632549, upload-time = "2025-12-29T17:26:57.598Z" },
+ { url = "https://files.pythonhosted.org/packages/75/b9/c823633afc48a1ac56d6ad34289c8f51b0234685142531bfa8197ca91777/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ade1f4127fdbe36a02f8067d75aa79c1ea1c8a306bf63c7b818bb7b530e1beaa", size = 495104, upload-time = "2025-12-29T17:26:58.826Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/8f/6f7030f18fa7307f87b0f57108a50a3a540b6350e2486d1739c0567629a3/backports_zstd-1.3.0-cp313-cp313-win32.whl", hash = "sha256:668e6fb1805b825cb7504c71436f7b28d4d792bb2663ee901ec9a2bb15804437", size = 288447, upload-time = "2025-12-29T17:27:00.036Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/82/b1df1bbbe4e6d3ffd364d0bcffdeb6c4361115c1eccd91238dbdd0c07fec/backports_zstd-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:385bdadf0ea8fe6ba780a95e4c7d7f018db7bafdd630932f0f9f0fad05d608ff", size = 313664, upload-time = "2025-12-29T17:27:01.267Z" },
+ { url = "https://files.pythonhosted.org/packages/45/0f/60918fe4d3f2881de8f4088d73be4837df9e4c6567594109d355a2d548b6/backports_zstd-1.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:4321a8a367537224b3559fe7aeb8012b98aea2a60a737e59e51d86e2e856fe0a", size = 288678, upload-time = "2025-12-29T17:27:02.506Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/b9/35f423c0bcd85020d5e7be6ab8d7517843e3e4441071beb5c3bd8c5216cb/backports_zstd-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:10057d66fa4f0a7d3f6419ffb84b4fe61088da572e3ac4446134a1c8089e4166", size = 436155, upload-time = "2025-12-29T17:27:03.859Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/14/e504daea24e8916f14ecbc223c354b558d8410cfc846606668ab91d96b38/backports_zstd-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4abf29d706ba05f658ca0247eb55675bcc00e10f12bca15736e45b05f1f2d2dc", size = 362436, upload-time = "2025-12-29T17:27:05.076Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/f7/06e178dbab7edb88c2872aebd68b54137e07a169eba1aeedf614014f7036/backports_zstd-1.3.0-cp313-cp313t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:127b0d73c745b0684da3d95c31c0939570810dad8967dfe8231eea8f0e047b2f", size = 507600, upload-time = "2025-12-29T17:27:06.254Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/f1/2ce499b81c4389d6fa1eeea7e76f6e0bad48effdbb239da7cbcdaaf24b76/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0205ef809fb38bb5ca7f59fa03993596f918768b9378fb7fbd8a68889a6ce028", size = 475496, upload-time = "2025-12-29T17:27:07.939Z" },
+ { url = "https://files.pythonhosted.org/packages/18/1e/c82a586f2866aabf3a601a521af3c58756d83d98b724fda200016ac5e7e2/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c389b667b0b07915781aa28beabf2481f11a6062a1a081873c4c443b98601a7", size = 580919, upload-time = "2025-12-29T17:27:09.1Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/a3/eb5d9b7c4cb69d1b8ccd011abe244ba6815693b70bed07ed4b77ddda4535/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8e7ac5ef693d49d6fb35cd7bbb98c4762cfea94a8bd2bf2ab112027004f70b11", size = 639913, upload-time = "2025-12-29T17:27:10.433Z" },
+ { url = "https://files.pythonhosted.org/packages/11/2c/7296b99df79d9f31174a99c81c1964a32de8996ce2b3068f5bc66b413615/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d5543945aae2a76a850b23f283249424f535de6a622d6002957b7d971e6a36d", size = 494800, upload-time = "2025-12-29T17:27:11.59Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/fc/b8ae6e104ba72d20cd5f9dfd9baee36675e89c81d432434927967114f30f/backports_zstd-1.3.0-cp313-cp313t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e38be15ebce82737deda2c9410c1f942f1df9da74121049243a009810432db75", size = 570396, upload-time = "2025-12-29T17:27:13.063Z" },
+ { url = "https://files.pythonhosted.org/packages/30/56/60a7a9de7a5bc951ea1106358b413c95183c93480394f3abc541313c8679/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3e3f58c76f4730607a4e0130d629173aa114ae72a5c8d3d5ad94e1bf51f18d8", size = 481980, upload-time = "2025-12-29T17:27:14.317Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/bb/93fc1e8e81b8ecba58b0e53a14f7b44375cf837db6354410998f0c4cb6ff/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:b808bf889722d889b792f7894e19c1f904bb0e9092d8c0eb0787b939b08bad9a", size = 511358, upload-time = "2025-12-29T17:27:15.669Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/0f/b165c2a6080d22306975cd86ce97270208493f31a298867e343110570370/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f7be27d56f2f715bcd252d0c65c232146d8e1e039c7e2835b8a3ad3dc88bc508", size = 585492, upload-time = "2025-12-29T17:27:16.986Z" },
+ { url = "https://files.pythonhosted.org/packages/26/76/85b4bde76e982b24a7eb57a2fb9868807887bef4d2114a3654a6530a67ef/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:cbe341c7fcc723893663a37175ba859328b907a4e6d2d40a4c26629cc55efb67", size = 568309, upload-time = "2025-12-29T17:27:18.28Z" },
+ { url = "https://files.pythonhosted.org/packages/83/64/9490667827a320766fb883f358a7c19171fdc04f19ade156a8c341c36967/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:b4116a9e12dfcd834dd9132cf6a94657bf0d328cba5b295f26de26ea0ae1adc8", size = 630518, upload-time = "2025-12-29T17:27:19.525Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/43/258587233b728bbff457bdb0c52b3e08504c485a8642b3daeb0bdd5a76bc/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1049e804cc8754290b24dab383d4d6ed0b7f794ad8338813ddcb3907d15a89d0", size = 499429, upload-time = "2025-12-29T17:27:21.063Z" },
+ { url = "https://files.pythonhosted.org/packages/32/04/cfab76878f360f124dbb533779e1e4603c801a0f5ada72ae5c742b7c4d7d/backports_zstd-1.3.0-cp313-cp313t-win32.whl", hash = "sha256:7d3f0f2499d2049ec53d2674c605a4b3052c217cc7ee49c05258046411685adc", size = 289389, upload-time = "2025-12-29T17:27:22.287Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/ff/dbcfb6c9c922ab6d98f3d321e7d0c7b34ecfa26f3ca71d930fe1ef639737/backports_zstd-1.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:eb2f8fab0b1ea05148394cb34a9e543a43477178765f2d6e7c84ed332e34935e", size = 314776, upload-time = "2025-12-29T17:27:23.458Z" },
+ { url = "https://files.pythonhosted.org/packages/01/4b/82e4baae3117806639fe1c693b1f2f7e6133a7cefd1fa2e38018c8edcd68/backports_zstd-1.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c66ad9eb5bfbe28c2387b7fc58ddcdecfb336d6e4e60bcba1694a906c1f21a6c", size = 289315, upload-time = "2025-12-29T17:27:24.601Z" },
+ { url = "https://files.pythonhosted.org/packages/95/b7/e843d32122f25d9568e75d1e7a29c00eae5e5728015604f3f6d02259b3a5/backports_zstd-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3ab0d5632b84eff4355c42a04668cfe6466f7d390890f718978582bd1ff36949", size = 409771, upload-time = "2025-12-29T17:27:48.869Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/a5/d6a897d4b91732f54b4506858f1da65d7a5b2dc0dbe36a23992a64f09f5a/backports_zstd-1.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b97cea95dbb1a97c02afd718155fad93f747815069722107a429804c355e206", size = 339289, upload-time = "2025-12-29T17:27:50.055Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/b0/f0ce566ec221b284508eebbf574a779ba4a8932830db6ea03b6176f336a2/backports_zstd-1.3.0-pp310-pypy310_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:477895f2642f9397aeba69618df2c91d7f336e02df83d1e623ac37c5d3a5115e", size = 420335, upload-time = "2025-12-29T17:27:51.455Z" },
+ { url = "https://files.pythonhosted.org/packages/62/6d/bf55652c84c79b2565d3087265bcb097719540a313dee16359a54d83ab4e/backports_zstd-1.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:330172aaf5fd3bfa53f49318abc6d1d4238cb043c384cf71f7b8f0fe2fb7ce31", size = 393880, upload-time = "2025-12-29T17:27:52.869Z" },
+ { url = "https://files.pythonhosted.org/packages/be/e0/d1feebb70ffeb150e2891c6f09700079f4a60085ebc67529eb1ca72fb5c2/backports_zstd-1.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32974e71eff15897ed3f8b7766a753d9f3197ea4f1c9025d80f8de099a691b99", size = 413840, upload-time = "2025-12-29T17:27:54.527Z" },
+ { url = "https://files.pythonhosted.org/packages/36/28/3b7be27ae51e418d3a724bbc4cb7fea77b6bd38b5007e333a56b0cb165c8/backports_zstd-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:993e3a34eaba5928a2065545e34bf75c65b9c34ecb67e43d5ef49b16cc182077", size = 299685, upload-time = "2025-12-29T17:27:56.149Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/d9/8c9c246e5ea79a4f45d551088b11b61f2dc7efcdc5dbe6df3be84a506e0c/backports_zstd-1.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:968167d29f012cee7b112ad031a8925e484e97e99288e55e4d62962c3a1013e3", size = 409666, upload-time = "2025-12-29T17:27:57.37Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/4f/a55b33c314ca8c9074e99daab54d04c5d212070ae7dbc435329baf1b139e/backports_zstd-1.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8f6fc7d62b71083b574193dd8fb3a60e6bb34880cc0132aad242943af301f7a", size = 339199, upload-time = "2025-12-29T17:27:58.542Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/13/ce31bd048b1c88d0f65d7af60b6cf89cfbed826c7c978f0ebca9a8a71cfc/backports_zstd-1.3.0-pp311-pypy311_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:e0f2eca6aac280fdb77991ad3362487ee91a7fb064ad40043fb5a0bf5a376943", size = 420332, upload-time = "2025-12-29T17:28:00.332Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/80/c0cdbc533d0037b57248588403a3afb050b2a83b8c38aa608e31b3a4d600/backports_zstd-1.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:676eb5e177d4ef528cf3baaeea4fffe05f664e4dd985d3ac06960ef4619c81a9", size = 393879, upload-time = "2025-12-29T17:28:01.57Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/38/c97428867cac058ed196ccaeddfdf82ecd43b8a65965f2950a6e7547e77a/backports_zstd-1.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:199eb9bd8aca6a9d489c41a682fad22c587dffe57b613d0fe6d492d0d38ce7c5", size = 413842, upload-time = "2025-12-29T17:28:03.113Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/ec/6247be6536668fe1c7dfae3eaa9c94b00b956b716957c0fc986ba78c3cc4/backports_zstd-1.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2524bd6777a828d5e7ccd7bd1a57f9e7007ae654fc2bd1bc1a207f6428674e4a", size = 299684, upload-time = "2025-12-29T17:28:04.856Z" },
+]
+
+[[package]]
+name = "blockbuster"
+version = "1.5.26"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "forbiddenfruit", marker = "python_full_version >= '3.11' and implementation_name == 'cpython'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e0/dcbab602790a576b0b94108c07e2c048e5897df7cc83722a89582d733987/blockbuster-1.5.26.tar.gz", hash = "sha256:cc3ce8c70fa852a97ee3411155f31e4ad2665cd1c6c7d2f8bb1851dab61dc629", size = 36085, upload-time = "2025-12-05T10:43:47.735Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/c1/84fc6811122f54b20de2e5afb312ee07a3a47a328755587d1e505475239b/blockbuster-1.5.26-py3-none-any.whl", hash = "sha256:f8e53fb2dd4b6c6ec2f04907ddbd063ca7cd1ef587d24448ef4e50e81e3a79bb", size = 13226, upload-time = "2025-12-05T10:43:48.778Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.1.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
+ { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
+ { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
+ { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
+ { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
+ { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
+ { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
+ { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
+ { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
+ { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
+ { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
+ { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
+ { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
+ { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
+ { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
+ { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
+ { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
+ { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
+ { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
+ { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
+ { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
+ { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
+ { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
+ { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
+ { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
+ { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
+ { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
+ { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
+ { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
+]
+
+[[package]]
+name = "cloudpickle"
+version = "3.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" },
+]
+
+[[package]]
+name = "codespell"
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740, upload-time = "2025-01-28T18:52:39.411Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501, upload-time = "2025-01-28T18:52:37.057Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "cryptography"
+version = "46.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
+ { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
+ { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
+ { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
+ { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
+ { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" },
+ { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
+ { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
+ { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
+ { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
+ { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
+ { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
+ { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
+ { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
+ { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
+ { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
+ { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" },
+ { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" },
+ { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" },
+ { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" },
+]
+
+[[package]]
+name = "distlib"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
+]
+
+[[package]]
+name = "docopt"
+version = "0.6.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901, upload-time = "2014-06-16T11:18:57.406Z" }
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.20.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
+]
+
+[[package]]
+name = "forbiddenfruit"
+version = "0.1.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e6/79/d4f20e91327c98096d605646bdc6a5ffedae820f38d378d3515c42ec5e60/forbiddenfruit-0.1.4.tar.gz", hash = "sha256:e3f7e66561a29ae129aac139a85d610dbf3dd896128187ed5454b6421f624253", size = 43756, upload-time = "2021-01-16T21:03:35.401Z" }
+
+[[package]]
+name = "googleapis-common-protos"
+version = "1.72.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "protobuf", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" },
+]
+
+[[package]]
+name = "grpcio"
+version = "1.76.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", size = 5840037, upload-time = "2025-10-21T16:20:25.069Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", size = 11836482, upload-time = "2025-10-21T16:20:30.113Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", size = 6407178, upload-time = "2025-10-21T16:20:32.733Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", size = 7075684, upload-time = "2025-10-21T16:20:35.435Z" },
+ { url = "https://files.pythonhosted.org/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", size = 6611133, upload-time = "2025-10-21T16:20:37.541Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", size = 7195507, upload-time = "2025-10-21T16:20:39.643Z" },
+ { url = "https://files.pythonhosted.org/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", size = 8160651, upload-time = "2025-10-21T16:20:42.492Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", size = 7620568, upload-time = "2025-10-21T16:20:45.995Z" },
+ { url = "https://files.pythonhosted.org/packages/90/84/7852f7e087285e3ac17a2703bc4129fafee52d77c6c82af97d905566857e/grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b", size = 3998879, upload-time = "2025-10-21T16:20:48.592Z" },
+ { url = "https://files.pythonhosted.org/packages/10/30/d3d2adcbb6dd3ff59d6ac3df6ef830e02b437fb5c90990429fd180e52f30/grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b", size = 4706892, upload-time = "2025-10-21T16:20:50.697Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" },
+ { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" },
+ { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" },
+ { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" },
+ { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" },
+ { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" },
+ { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" },
+ { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" },
+ { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" },
+ { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" },
+ { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" },
+ { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" },
+ { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" },
+ { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" },
+ { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" },
+ { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" },
+]
+
+[[package]]
+name = "grpcio-health-checking"
+version = "1.76.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "grpcio", marker = "python_full_version >= '3.11'" },
+ { name = "protobuf", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3e/96/5a52dcf21078b47ffa0c2ed613c3153a06f138edb6133792bace5f1ccc1d/grpcio_health_checking-1.76.0.tar.gz", hash = "sha256:b7a99d74096b3ab3a59987fc02374068e1c180a352e8d1f79f10e5a23727098d", size = 16784, upload-time = "2025-10-21T16:28:55.204Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/65/e6/746dffa51399827e38bb3f3f1ad656a3d8c1255039b256a6f76593368768/grpcio_health_checking-1.76.0-py3-none-any.whl", hash = "sha256:9743f345a855ba030cc7c381361606870b79d33bb71d7756efa47b6faa970f81", size = 18910, upload-time = "2025-10-21T16:27:26.332Z" },
+]
+
+[[package]]
+name = "grpcio-tools"
+version = "1.75.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "grpcio", marker = "python_full_version >= '3.11'" },
+ { name = "protobuf", marker = "python_full_version >= '3.11'" },
+ { name = "setuptools", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7d/76/0cd2a2bb379275c319544a3ab613dc3cea7a167503908c1b4de55f82bd9e/grpcio_tools-1.75.1.tar.gz", hash = "sha256:bb78960cf3d58941e1fec70cbdaccf255918beed13c34112a6915a6d8facebd1", size = 5390470, upload-time = "2025-09-26T09:10:11.948Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/b7/7d1b0b7669f993a6a393083a876937f478ca034c283eb23baf6720d8c85a/grpcio_tools-1.75.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:ae0f04d5ec8b8e13476bf516a08fc1de4e58c6bf79f99123a6b964ca7d02c790", size = 2545419, upload-time = "2025-09-26T09:07:44.432Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/c0/db5d052d1ba5e859c833d1366960d784c0b44c8330012717aeaa123b6b9f/grpcio_tools-1.75.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:24a881ad7292e904fc256892b647da17d9137ef2e72faf8b7c8e515314ad1377", size = 5841650, upload-time = "2025-09-26T09:07:50.987Z" },
+ { url = "https://files.pythonhosted.org/packages/af/13/ab49230ef106f2b9de156a813bc14049e6fd4fe9c26fa0cde496f0e86a09/grpcio_tools-1.75.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1b5810ace274dba12ecfac69ac32c8047c6ee0200a23274cb4885ed4187271f8", size = 2591560, upload-time = "2025-09-26T09:07:52.777Z" },
+ { url = "https://files.pythonhosted.org/packages/29/fd/1fd3069fb0559c2f90d85b0fd3a73adc3f63966c6300fee01e4e52740229/grpcio_tools-1.75.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ab33993288b97b1180e092fa447a8ce00fbc8c59d67b23553245b88d14fe36bb", size = 2904895, upload-time = "2025-09-26T09:07:55.002Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/51/e58fae40132a4589819c388333545a33a89f91b8affcac45623ace9ca659/grpcio_tools-1.75.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4cac693621043ef11d3ab2318e811d919779f8cd5011ba8e37f44c178c831d94", size = 2656151, upload-time = "2025-09-26T09:07:56.762Z" },
+ { url = "https://files.pythonhosted.org/packages/39/c9/a33736c2a8ceee39991f2c9f67a426ab799c6caf09145120bae6080428d5/grpcio_tools-1.75.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a09cd5d267b296af67116fe098633ad770bc8c19831a5f3c896f65fad90c1064", size = 3105152, upload-time = "2025-09-26T09:07:58.702Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/71/2f09cbe321f057a47a8ae4dacf004bbe8a171fd712b8f7f689ec7b2f1c49/grpcio_tools-1.75.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dff4bcb4d16cf9ef745c1984394ed15187e6c23d73d71377377deaf443d11358", size = 3654551, upload-time = "2025-09-26T09:08:00.87Z" },
+ { url = "https://files.pythonhosted.org/packages/05/54/91481a5b96cab2a81326ab1041fd7cfc6b6ce0cd82bab14ebcdb6ed78d4e/grpcio_tools-1.75.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:16d5986b37e2a9203f85e456c7ff8705b932718021d408adfe4a79e0f4d95949", size = 3322220, upload-time = "2025-09-26T09:08:02.724Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/07/272955f15a35ef0069ebe17a5fc14282c6dc6690edeb4dbe6a81dd2d1efb/grpcio_tools-1.75.1-cp310-cp310-win32.whl", hash = "sha256:3fbac14998bfadc6b9140b6339dbc5f673700ebb4d45ba0c4d4fbe0ffb8559a9", size = 992986, upload-time = "2025-09-26T09:08:04.6Z" },
+ { url = "https://files.pythonhosted.org/packages/16/9a/482b05c1277b3385be7e426f000efb921ce3ae76bcb8aa4f9b9f724c58d3/grpcio_tools-1.75.1-cp310-cp310-win_amd64.whl", hash = "sha256:b56e495844eb899de721eb77d9e077192bdeb40842f598481d32a8f6de3db124", size = 1157427, upload-time = "2025-09-26T09:08:06.246Z" },
+ { url = "https://files.pythonhosted.org/packages/45/28/71ab934662d41ded4e451d9af0ec6f9aade3525e470fdfd10bd20e588e44/grpcio_tools-1.75.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:f0635231feb70a9d551452829943a1a5fa651283e7a300aadc22df5ea5da696f", size = 2545461, upload-time = "2025-09-26T09:08:08.514Z" },
+ { url = "https://files.pythonhosted.org/packages/69/40/d90f6fdb51f51b2a518401207b3920fcfdfa996ed7bca844096f111ed839/grpcio_tools-1.75.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:626293296ef7e2d87ab1a80b81a55eef91883c65b59a97576099a28b9535100b", size = 5842958, upload-time = "2025-09-26T09:08:11.468Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/b7/52e6f32fd0101e3ac9c654a6441b254ba5874f146b543b20afbcb8246947/grpcio_tools-1.75.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:071339d90f1faab332ce4919c815a10b9c3ed2c09473f550f686bf9cc148579f", size = 2591669, upload-time = "2025-09-26T09:08:13.481Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/3c/115c59a5c0c8e9d7d99a40bac8d5e91c05b6735b3bb185265d40e9fc4346/grpcio_tools-1.75.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:44195f58c052fa935b78c7438c85cbcd4b273dd685028e4f6d4d7b30d47daad1", size = 2904952, upload-time = "2025-09-26T09:08:15.299Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/cd/d2a3583a5b1d71da88f7998f20fb5a0b6fe5bb96bb916a610c29269063b6/grpcio_tools-1.75.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:860fafdb85726029d646c99859ff7bdca5aae61b5ff038c3bd355fc1ec6b2764", size = 2656311, upload-time = "2025-09-26T09:08:17.094Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/09/67b9215d39add550e430c9677bd43c9a315da07ab62fa3a5f44f1cf5bb75/grpcio_tools-1.75.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4559547a0cb3d3db1b982eea87d4656036339b400f48127fef932210672fb59e", size = 3105583, upload-time = "2025-09-26T09:08:19.179Z" },
+ { url = "https://files.pythonhosted.org/packages/98/d7/d400b90812470f3dc2466964e62fc03592de46b5c824c82ef5303be60167/grpcio_tools-1.75.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9af65a310807d7f36a8f7cddea142fe97d6dffba74444f38870272f2e5a3a06b", size = 3654677, upload-time = "2025-09-26T09:08:21.227Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/93/edf6de71b4f936b3f09461a3286db1f902c6366c5de06ef19a8c2523034a/grpcio_tools-1.75.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c1de31aefc0585d2f915a7cd0994d153547495b8d79c44c58048a3ede0b65be", size = 3322147, upload-time = "2025-09-26T09:08:23.08Z" },
+ { url = "https://files.pythonhosted.org/packages/80/00/0f8c6204e34070e7d4f344b27e4b1b0320dfdd94574f79738a43504d182e/grpcio_tools-1.75.1-cp311-cp311-win32.whl", hash = "sha256:efaf95fcaa5d3ac1bcfe44ceed9e2512eb95b5c8c476569bdbbe2bee4b59c8a9", size = 993388, upload-time = "2025-09-26T09:08:24.708Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/ae/6f738154980f606293988a64ef4bb0ea2bb12029a4529464aac56fe2ab99/grpcio_tools-1.75.1-cp311-cp311-win_amd64.whl", hash = "sha256:7cefe76fc35c825f0148d60d2294a527053d0f5dd6a60352419214a8c53223c9", size = 1157907, upload-time = "2025-09-26T09:08:26.537Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/a7/581bb204d19a347303ed5e25b19f7d8c6365a28c242fca013d1d6d78ad7e/grpcio_tools-1.75.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:49b68936cf212052eeafa50b824e17731b78d15016b235d36e0d32199000b14c", size = 2546099, upload-time = "2025-09-26T09:08:28.794Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/59/ab65998eba14ff9d292c880f6a276fe7d0571bba3bb4ddf66aca1f8438b5/grpcio_tools-1.75.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:08cb6e568e58b76a2178ad3b453845ff057131fff00f634d7e15dcd015cd455b", size = 5839838, upload-time = "2025-09-26T09:08:31.038Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/65/7027f71069b4c1e8c7b46de8c46c297c9d28ef6ed4ea0161e8c82c75d1d0/grpcio_tools-1.75.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:168402ad29a249092673079cf46266936ec2fb18d4f854d96e9c5fa5708efa39", size = 2592916, upload-time = "2025-09-26T09:08:33.216Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/84/1abfb3c679b78c7fca7524031cf9de4c4c509c441b48fd26291ac16dd1af/grpcio_tools-1.75.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:bbae11c29fcf450730f021bfc14b12279f2f985e2e493ccc2f133108728261db", size = 2905276, upload-time = "2025-09-26T09:08:35.691Z" },
+ { url = "https://files.pythonhosted.org/packages/99/cd/7f9e05f1eddccb61bc0ead1e49eb2222441957b02ed11acfcd2f795b03a8/grpcio_tools-1.75.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38c6c7d5d4800f636ee691cd073db1606d1a6a76424ca75c9b709436c9c20439", size = 2656424, upload-time = "2025-09-26T09:08:38.255Z" },
+ { url = "https://files.pythonhosted.org/packages/29/1d/8b7852771c2467728341f7b9c3ca4ebc76e4e23485c6a3e6d97a8323ad2a/grpcio_tools-1.75.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:626f6a61a8f141dde9a657775854d1c0d99509f9a2762b82aa401a635f6ec73d", size = 3108985, upload-time = "2025-09-26T09:08:40.291Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/6a/069da89cdf2e97e4558bfceef5b60bf0ef200c443b465e7691869006dd32/grpcio_tools-1.75.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f61a8334ae38d4f98c744a732b89527e5af339d17180e25fff0676060f8709b7", size = 3657940, upload-time = "2025-09-26T09:08:42.437Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/e4/ca8dae800c084beb89e2720346f70012d36dfb9df02d8eacd518c06cf4a0/grpcio_tools-1.75.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd0c3fb40d89a1e24a41974e77c7331e80396ab7cde39bc396a13d6b5e2a750b", size = 3324878, upload-time = "2025-09-26T09:08:45.083Z" },
+ { url = "https://files.pythonhosted.org/packages/58/06/cbe923679309bf970923f4a11351ea9e485291b504d7243130fdcfdcb03f/grpcio_tools-1.75.1-cp312-cp312-win32.whl", hash = "sha256:004bc5327593eea48abd03be3188e757c3ca0039079587a6aac24275127cac20", size = 993071, upload-time = "2025-09-26T09:08:46.785Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/0c/84d6be007262c5d88a590082f3a1fe62d4b0eeefa10c6cdb3548f3663e80/grpcio_tools-1.75.1-cp312-cp312-win_amd64.whl", hash = "sha256:23952692160b5fe7900653dfdc9858dc78c2c42e15c27e19ee780c8917ba6028", size = 1157506, upload-time = "2025-09-26T09:08:48.844Z" },
+ { url = "https://files.pythonhosted.org/packages/47/fa/624bbe1b2ccf4f6044bf3cd314fe2c35f78f702fcc2191dc65519baddca4/grpcio_tools-1.75.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:ca9e116aab0ecf4365fc2980f2e8ae1b22273c3847328b9a8e05cbd14345b397", size = 2545752, upload-time = "2025-09-26T09:08:51.433Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/4c/6d884e2337feff0a656e395338019adecc3aa1daeae9d7e8eb54340d4207/grpcio_tools-1.75.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:9fe87a926b65eb7f41f8738b6d03677cc43185ff77a9d9b201bdb2f673f3fa1e", size = 5838163, upload-time = "2025-09-26T09:08:53.858Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/2a/2ba7b6911a754719643ed92ae816a7f989af2be2882b9a9e1f90f4b0e882/grpcio_tools-1.75.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:45503a6094f91b3fd31c3d9adef26ac514f102086e2a37de797e220a6791ee87", size = 2592148, upload-time = "2025-09-26T09:08:55.86Z" },
+ { url = "https://files.pythonhosted.org/packages/88/db/fa613a45c3c7b00f905bd5ad3a93c73194724d0a2dd72adae3be32983343/grpcio_tools-1.75.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b01b60b3de67be531a39fd869d7613fa8f178aff38c05e4d8bc2fc530fa58cb5", size = 2905215, upload-time = "2025-09-26T09:08:58.27Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/0c/ee4786972bb82f60e4f313bb2227c79c2cd20eb13c94c0263067923cfd12/grpcio_tools-1.75.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e2b9b9488735514777d44c1e4eda813122d2c87aad219f98d5d49b359a8eab", size = 2656251, upload-time = "2025-09-26T09:09:00.249Z" },
+ { url = "https://files.pythonhosted.org/packages/77/f1/cc5a50658d705d0b71ff8a4fbbfcc6279d3c95731a2ef7285e13dc40e2fe/grpcio_tools-1.75.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:55e60300e62b220fabe6f062fe69f143abaeff3335f79b22b56d86254f3c3c80", size = 3108911, upload-time = "2025-09-26T09:09:02.515Z" },
+ { url = "https://files.pythonhosted.org/packages/09/d8/43545f77c4918e778e90bc2c02b3462ac71cee14f29d85cdb69b089538eb/grpcio_tools-1.75.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:49ce00fcc6facbbf52bf376e55b8e08810cecd03dab0b3a2986d73117c6f6ee4", size = 3657021, upload-time = "2025-09-26T09:09:05.331Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/0b/2ae5925374b66bc8df5b828eff1a5f9459349c83dae1773f0aa9858707e6/grpcio_tools-1.75.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71e95479aea868f8c8014d9dc4267f26ee75388a0d8a552e1648cfa0b53d24b4", size = 3324450, upload-time = "2025-09-26T09:09:07.867Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/53/9f887bacbecf892ac5b0b282477ca8cfa5b73911b04259f0d88b52e9a055/grpcio_tools-1.75.1-cp313-cp313-win32.whl", hash = "sha256:fff9d2297416eae8861e53154ccf70a19994e5935e6c8f58ebf431f81cbd8d12", size = 992434, upload-time = "2025-09-26T09:09:09.966Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/f0/9979d97002edffdc2a88e5f2e0dccea396dd4a6eab34fa2f705fe43eae2f/grpcio_tools-1.75.1-cp313-cp313-win_amd64.whl", hash = "sha256:1849ddd508143eb48791e81d42ddc924c554d1b4900e06775a927573a8d4267f", size = 1157069, upload-time = "2025-09-26T09:09:12.287Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/0b/4ff4ead293f2b016668628a240937828444094778c8037d2bbef700e9097/grpcio_tools-1.75.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:f281b594489184b1f9a337cdfed1fc1ddb8428f41c4b4023de81527e90b38e1e", size = 2545868, upload-time = "2025-09-26T09:09:14.716Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/78/aa6bf73a18de5357c01ef87eea92150931586b25196fa4df197a37bae11d/grpcio_tools-1.75.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:becf8332f391abc62bf4eea488b63be063d76a7cf2ef00b2e36c617d9ee9216b", size = 5838010, upload-time = "2025-09-26T09:09:20.415Z" },
+ { url = "https://files.pythonhosted.org/packages/99/65/7eaad673bc971af45e079d3b13c20d9ba9842b8788d31953e3234c2e2cee/grpcio_tools-1.75.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a08330f24e5cd7b39541882a95a8ba04ffb4df79e2984aa0cd01ed26dcdccf49", size = 2593170, upload-time = "2025-09-26T09:09:22.889Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/db/57e1e29e9186c7ed223ce8a9b609d3f861c4db015efb643dfe60b403c137/grpcio_tools-1.75.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6bf3742bd8f102630072ed317d1496f31c454cd85ad19d37a68bd85bf9d5f8b9", size = 2905167, upload-time = "2025-09-26T09:09:25.96Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/7b/894f891f3cf19812192f8bbf1e0e1c958055676ecf0a5466a350730a006d/grpcio_tools-1.75.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f26028949474feb380460ce52d9d090d00023940c65236294a66c42ac5850e8b", size = 2656210, upload-time = "2025-09-26T09:09:28.786Z" },
+ { url = "https://files.pythonhosted.org/packages/99/76/8e48427da93ef243c09629969c7b5a2c59dceb674b6b623c1f5fbaa5c8c5/grpcio_tools-1.75.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1bd68fb98bf08f11b6c3210834a14eefe585bad959bdba38e78b4ae3b04ba5bd", size = 3109226, upload-time = "2025-09-26T09:09:31.307Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/7e/ecf71c316c2a88c2478b7c6372d0f82d05f07edbf0f31b6da613df99ec7c/grpcio_tools-1.75.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f1496e21586193da62c3a73cd16f9c63c5b3efd68ff06dab96dbdfefa90d40bf", size = 3657139, upload-time = "2025-09-26T09:09:35.043Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/f3/b2613e81da2085f40a989c0601ec9efc11e8b32fcb71b1234b64a18af830/grpcio_tools-1.75.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:14a78b1e36310cdb3516cdf9ee2726107875e0b247e2439d62fc8dc38cf793c1", size = 3324513, upload-time = "2025-09-26T09:09:37.44Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/1f/2df4fa8634542524bc22442ffe045d41905dae62cc5dd14408b80c5ac1b8/grpcio_tools-1.75.1-cp314-cp314-win32.whl", hash = "sha256:0e6f916daf222002fb98f9a6f22de0751959e7e76a24941985cc8e43cea77b50", size = 1015283, upload-time = "2025-09-26T09:09:39.461Z" },
+ { url = "https://files.pythonhosted.org/packages/23/4f/f27c973ff50486a70be53a3978b6b0244398ca170a4e19d91988b5295d92/grpcio_tools-1.75.1-cp314-cp314-win_amd64.whl", hash = "sha256:878c3b362264588c45eba57ce088755f8b2b54893d41cc4a68cdeea62996da5c", size = 1189364, upload-time = "2025-09-26T09:09:42.036Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "hatch"
+version = "1.16.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "backports-zstd", marker = "python_full_version < '3.14'" },
+ { name = "click" },
+ { name = "hatchling" },
+ { name = "httpx" },
+ { name = "hyperlink" },
+ { name = "keyring" },
+ { name = "packaging" },
+ { name = "pexpect" },
+ { name = "platformdirs" },
+ { name = "pyproject-hooks" },
+ { name = "rich" },
+ { name = "shellingham" },
+ { name = "tomli-w" },
+ { name = "tomlkit" },
+ { name = "userpath" },
+ { name = "uv" },
+ { name = "virtualenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/41/c1/976b807478878d31d467dd17b9fe642962f292e16ed13c34b593c0453fde/hatch-1.16.3.tar.gz", hash = "sha256:2a50ecc912adfc8122cd2ccdcc15254cdef829e5d158be9014180cd7f0fb7ea9", size = 5219621, upload-time = "2026-01-21T01:36:19.822Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b8/b4/5c5fa4ca8c59e7ef0a224ff10e6336e73ca61c5e0eff09ee691441c9275f/hatch-1.16.3-py3-none-any.whl", hash = "sha256:f5169025cf1cdfe981366eb96127cab1d1bc59f5f2acb87c4cc308c25d95a4b1", size = 141305, upload-time = "2026-01-21T01:36:18.13Z" },
+]
+
+[[package]]
+name = "hatchling"
+version = "1.28.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "pluggy" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "trove-classifiers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0b/8e/e480359492affde4119a131da729dd26da742c2c9b604dff74836e47eef9/hatchling-1.28.0.tar.gz", hash = "sha256:4d50b02aece6892b8cd0b3ce6c82cb218594d3ec5836dbde75bf41a21ab004c8", size = 55365, upload-time = "2025-11-27T00:31:13.766Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl", hash = "sha256:dc48722b68b3f4bbfa3ff618ca07cdea6750e7d03481289ffa8be1521d18a961", size = 76075, upload-time = "2025-11-27T00:31:12.544Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "hyperlink"
+version = "21.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "importlib-metadata"
+version = "8.7.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "zipp" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "jaraco-classes"
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "more-itertools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" },
+]
+
+[[package]]
+name = "jaraco-context"
+version = "6.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "backports-tarfile", marker = "python_full_version < '3.12'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" },
+]
+
+[[package]]
+name = "jaraco-functools"
+version = "4.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "more-itertools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" },
+]
+
+[[package]]
+name = "jeepney"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
+]
+
+[[package]]
+name = "jsonpatch"
+version = "1.33"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonpointer", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" },
+]
+
+[[package]]
+name = "jsonpointer"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" },
+]
+
+[[package]]
+name = "jsonschema-rs"
+version = "0.29.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/b4/33a9b25cad41d1e533c1ab7ff30eaec50628dd1bcb92171b99a2e944d61f/jsonschema_rs-0.29.1.tar.gz", hash = "sha256:a9f896a9e4517630374f175364705836c22f09d5bd5bbb06ec0611332b6702fd", size = 1406679, upload-time = "2025-02-08T21:25:12.639Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4e/2a/cc53cf158d9ea3629b0c818fdbce9ad2cd8f656f11cafd0e21124487887e/jsonschema_rs-0.29.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7d84c4e1483a103694150b5706d638606443c814662738f14d34ca16948349df", size = 3828955, upload-time = "2025-02-08T21:23:47.957Z" },
+ { url = "https://files.pythonhosted.org/packages/83/81/6ff994c6bf223eab69109f367fe8c5551a84f9e17869b6d51682776916fa/jsonschema_rs-0.29.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:0e7b72365ae0875c0e99f951ad4cec00c6f5e57b25ed5b8495b8f2c810ad21a6", size = 1968807, upload-time = "2025-02-08T21:23:52.069Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/11/28243681ff1337bfe988d5b13acc11bd7f962a16eb5721f4fba86c8c7157/jsonschema_rs-0.29.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7078d3cc635b9832ba282ec4de3af4cdaba4af74691e52aa3ea5f7d1baaa3b76", size = 2066165, upload-time = "2025-02-08T21:23:55.573Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/17/263f9dccc3d80b7b43ee564b414155a62f6685eb2e657a3d1ebb68f791af/jsonschema_rs-0.29.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ed43c64e0d732edf32a881f0c1db340c9484f3a28c41f7da96667a49eb0f34", size = 2067619, upload-time = "2025-02-08T21:23:58.528Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/a7/00f4fec03168c71206bf9da3ff9d943cb2b44ad95f44255c4ae2efcdd4a0/jsonschema_rs-0.29.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a14806090a5ebf1616a0047eb8049f70f3a830c460e71d5f4a78b300644ec9", size = 2085023, upload-time = "2025-02-08T21:24:00.133Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/d9/ae4b6de008b45827e534f90cd68f9f34702bae02f9050101e556683d3c55/jsonschema_rs-0.29.1-cp310-cp310-win32.whl", hash = "sha256:3d739419212e87219e0aa5b9b81eee726e755f606ac63f4795e37efeb9635ed9", size = 1704378, upload-time = "2025-02-08T21:24:02.611Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/ca/4b8df88f81f560d712144f1dd2526852fc2d183eb5ae044c524146666d68/jsonschema_rs-0.29.1-cp310-cp310-win_amd64.whl", hash = "sha256:a8fa9007e76cea86877165ebb13ed94648246a185d5eabaf9125e97636bc56e4", size = 1872145, upload-time = "2025-02-08T21:24:05.489Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/e2/9c3af8c7d56ff1b6bac88137f60bf02f2814c60d1f658ef06b2ddc2a21b1/jsonschema_rs-0.29.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b4458f1a027ab0c64e91edcb23c48220d60a503e741030bcf260fbbe12979ad2", size = 3828925, upload-time = "2025-02-08T21:24:07.289Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/29/f9377e55f10ef173c4cf1c2c88bc30e4a1a4ea1c60659c524903cac85a07/jsonschema_rs-0.29.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:faf3d90b5473bf654fd6ffb490bd6fdd2e54f4034f652d1749bee963b3104ce3", size = 1968915, upload-time = "2025-02-08T21:24:09.123Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/ae/8c514ebab1d312a2422bece0a1ccca45b82a36131d4cb63e01b4469ac99a/jsonschema_rs-0.29.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e96919483960737ea5cd8d36e0752c63b875459f31ae14b3a6e80df925b74947", size = 2066366, upload-time = "2025-02-08T21:24:10.469Z" },
+ { url = "https://files.pythonhosted.org/packages/05/3e/04c6b25ae1b53c8c72eaf35cdda4f84558ca4df011d370b5906a6f56ba7f/jsonschema_rs-0.29.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e70f1ff7281810327b354ecaeba6cdce7fe498483338207fe7edfae1b21c212", size = 2067599, upload-time = "2025-02-08T21:24:12.006Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/78/b9b8934e4db4f43f61e65c5f285432c2d07cb1935ad9df88d5080a4a311b/jsonschema_rs-0.29.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07fef0706a5df7ba5f301a6920b28b0a4013ac06623aed96a6180e95c110b82a", size = 2084926, upload-time = "2025-02-08T21:24:14.544Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/ae/676d67d2583cdd50b07b5a0989b501aebf003b12232d14f87fc7fb991f2c/jsonschema_rs-0.29.1-cp311-cp311-win32.whl", hash = "sha256:07524370bdce055d4f106b7fed1afdfc86facd7d004cbb71adeaff3e06861bf6", size = 1704339, upload-time = "2025-02-08T21:24:16.145Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/3e/4767dce237d8ea2ff5f684699ef1b9dae5017dc41adaa6f3dc3a85b84608/jsonschema_rs-0.29.1-cp311-cp311-win_amd64.whl", hash = "sha256:36fa23c85333baa8ce5bf0564fb19de3d95b7640c0cab9e3205ddc44a62fdbf0", size = 1872253, upload-time = "2025-02-08T21:24:18.43Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/4a/67ea15558ab85e67d1438b2e5da63b8e89b273c457106cbc87f8f4959a3d/jsonschema_rs-0.29.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9fe7529faa6a84d23e31b1f45853631e4d4d991c85f3d50e6d1df857bb52b72d", size = 3825206, upload-time = "2025-02-08T21:24:19.985Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/2e/bc75ed65d11ba47200ade9795ebd88eb2e64c2852a36d9be640172563430/jsonschema_rs-0.29.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5d7e385298f250ed5ce4928fd59fabf2b238f8167f2c73b9414af8143dfd12e", size = 1966302, upload-time = "2025-02-08T21:24:21.673Z" },
+ { url = "https://files.pythonhosted.org/packages/95/dd/4a90e96811f897de066c69d95bc0983138056b19cb169f2a99c736e21933/jsonschema_rs-0.29.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:64a29be0504731a2e3164f66f609b9999aa66a2df3179ecbfc8ead88e0524388", size = 2062846, upload-time = "2025-02-08T21:24:23.171Z" },
+ { url = "https://files.pythonhosted.org/packages/21/91/61834396748a741021716751a786312b8a8319715e6c61421447a07c887c/jsonschema_rs-0.29.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e91defda5dfa87306543ee9b34d97553d9422c134998c0b64855b381f8b531d", size = 2065564, upload-time = "2025-02-08T21:24:24.574Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/2c/920d92e88b9bdb6cb14867a55e5572e7b78bfc8554f9c625caa516aa13dd/jsonschema_rs-0.29.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96f87680a6a1c16000c851d3578534ae3c154da894026c2a09a50f727bd623d4", size = 2083055, upload-time = "2025-02-08T21:24:26.834Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/0a/f4c1bea3193992fe4ff9ce330c6a594481caece06b1b67d30b15992bbf54/jsonschema_rs-0.29.1-cp312-cp312-win32.whl", hash = "sha256:bcfc0d52ecca6c1b2fbeede65c1ad1545de633045d42ad0c6699039f28b5fb71", size = 1701065, upload-time = "2025-02-08T21:24:28.282Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/89/3f89de071920208c0eb64b827a878d2e587f6a3431b58c02f63c3468b76e/jsonschema_rs-0.29.1-cp312-cp312-win_amd64.whl", hash = "sha256:a414c162d687ee19171e2d8aae821f396d2f84a966fd5c5c757bd47df0954452", size = 1871774, upload-time = "2025-02-08T21:24:30.824Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/9b/d642024e8b39753b789598363fd5998eb3053b52755a5df6a021d53741d5/jsonschema_rs-0.29.1-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0afee5f31a940dec350a33549ec03f2d1eda2da3049a15cd951a266a57ef97ee", size = 3824864, upload-time = "2025-02-08T21:24:32.252Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/3d/48a7baa2373b941e89a12e720dae123fd0a663c28c4e82213a29c89a4715/jsonschema_rs-0.29.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:c38453a5718bcf2ad1b0163d128814c12829c45f958f9407c69009d8b94a1232", size = 1966084, upload-time = "2025-02-08T21:24:33.8Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/e4/f260917a17bb28bb1dec6fa5e869223341fac2c92053aa9bd23c1caaefa0/jsonschema_rs-0.29.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5dc8bdb1067bf4f6d2f80001a636202dc2cea027b8579f1658ce8e736b06557f", size = 2062430, upload-time = "2025-02-08T21:24:35.174Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/e7/61353403b76768601d802afa5b7b5902d52c33d1dd0f3159aafa47463634/jsonschema_rs-0.29.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bcfe23992623a540169d0845ea8678209aa2fe7179941dc7c512efc0c2b6b46", size = 2065443, upload-time = "2025-02-08T21:24:36.778Z" },
+ { url = "https://files.pythonhosted.org/packages/40/ed/40b971a09f46a22aa956071ea159413046e9d5fcd280a5910da058acdeb2/jsonschema_rs-0.29.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f2a526c0deacd588864d3400a0997421dffef6fe1df5cfda4513a453c01ad42", size = 2082606, upload-time = "2025-02-08T21:24:38.388Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/59/1c142e1bfb87d57c18fb189149f7aa8edf751725d238d787015278b07600/jsonschema_rs-0.29.1-cp313-cp313-win32.whl", hash = "sha256:68acaefb54f921243552d15cfee3734d222125584243ca438de4444c5654a8a3", size = 1700666, upload-time = "2025-02-08T21:24:40.573Z" },
+ { url = "https://files.pythonhosted.org/packages/13/e8/f0ad941286cd350b879dd2b3c848deecd27f0b3fbc0ff44f2809ad59718d/jsonschema_rs-0.29.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c4e5a61ac760a2fc3856a129cc84aa6f8fba7b9bc07b19fe4101050a8ecc33c", size = 1871619, upload-time = "2025-02-08T21:24:42.286Z" },
+]
+
+[[package]]
+name = "keyring"
+version = "25.7.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata", marker = "python_full_version < '3.12'" },
+ { name = "jaraco-classes" },
+ { name = "jaraco-context" },
+ { name = "jaraco-functools" },
+ { name = "jeepney", marker = "sys_platform == 'linux'" },
+ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
+ { name = "secretstorage", marker = "sys_platform == 'linux'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" },
+]
+
+[[package]]
+name = "langchain-core"
+version = "1.2.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonpatch", marker = "python_full_version >= '3.11'" },
+ { name = "langsmith", marker = "python_full_version >= '3.11'" },
+ { name = "packaging", marker = "python_full_version >= '3.11'" },
+ { name = "pydantic", marker = "python_full_version >= '3.11'" },
+ { name = "pyyaml", marker = "python_full_version >= '3.11'" },
+ { name = "tenacity", marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version >= '3.11'" },
+ { name = "uuid-utils", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a2/0e/664d8d81b3493e09cbab72448d2f9d693d1fa5aa2bcc488602203a9b6da0/langchain_core-1.2.7.tar.gz", hash = "sha256:e1460639f96c352b4a41c375f25aeb8d16ffc1769499fb1c20503aad59305ced", size = 837039, upload-time = "2026-01-09T17:44:25.505Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6e/6f/34a9fba14d191a67f7e2ee3dbce3e9b86d2fa7310e2c7f2c713583481bd2/langchain_core-1.2.7-py3-none-any.whl", hash = "sha256:452f4fef7a3d883357b22600788d37e3d8854ef29da345b7ac7099f33c31828b", size = 490232, upload-time = "2026-01-09T17:44:24.236Z" },
+]
+
+[[package]]
+name = "langgraph"
+version = "1.0.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "langchain-core", marker = "python_full_version >= '3.11'" },
+ { name = "langgraph-checkpoint", marker = "python_full_version >= '3.11'" },
+ { name = "langgraph-prebuilt", marker = "python_full_version >= '3.11'" },
+ { name = "langgraph-sdk", marker = "python_full_version >= '3.11'" },
+ { name = "pydantic", marker = "python_full_version >= '3.11'" },
+ { name = "xxhash", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/5b/f72655717c04e33d3b62f21b166dc063d192b53980e9e3be0e2a117f1c9f/langgraph-1.0.7.tar.gz", hash = "sha256:0cfdfee51e6e8cfe503ecc7367c73933437c505b03fa10a85c710975c8182d9a", size = 497098, upload-time = "2026-01-22T16:57:47.303Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/0e/fe80144e3e4048e5d19ccdb91ac547c1a7dc3da8dbd1443e210048194c14/langgraph-1.0.7-py3-none-any.whl", hash = "sha256:9d68e8f8dd8f3de2fec45f9a06de05766d9b075b78fb03171779893b7a52c4d2", size = 157353, upload-time = "2026-01-22T16:57:45.997Z" },
+]
+
+[[package]]
+name = "langgraph-api"
+version = "0.7.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cloudpickle", marker = "python_full_version >= '3.11'" },
+ { name = "cryptography", marker = "python_full_version >= '3.11'" },
+ { name = "grpcio", marker = "python_full_version >= '3.11'" },
+ { name = "grpcio-health-checking", marker = "python_full_version >= '3.11'" },
+ { name = "grpcio-tools", marker = "python_full_version >= '3.11'" },
+ { name = "httpx", marker = "python_full_version >= '3.11'" },
+ { name = "jsonschema-rs", marker = "python_full_version >= '3.11'" },
+ { name = "langchain-core", marker = "python_full_version >= '3.11'" },
+ { name = "langgraph", marker = "python_full_version >= '3.11'" },
+ { name = "langgraph-checkpoint", marker = "python_full_version >= '3.11'" },
+ { name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11'" },
+ { name = "langgraph-sdk", marker = "python_full_version >= '3.11'" },
+ { name = "langsmith", extra = ["otel"], marker = "python_full_version >= '3.11'" },
+ { name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
+ { name = "opentelemetry-exporter-otlp-proto-http", marker = "python_full_version >= '3.11'" },
+ { name = "opentelemetry-sdk", marker = "python_full_version >= '3.11'" },
+ { name = "orjson", marker = "python_full_version >= '3.11'" },
+ { name = "protobuf", marker = "python_full_version >= '3.11'" },
+ { name = "pyjwt", marker = "python_full_version >= '3.11'" },
+ { name = "sse-starlette", marker = "python_full_version >= '3.11'" },
+ { name = "starlette", marker = "python_full_version >= '3.11'" },
+ { name = "structlog", marker = "python_full_version >= '3.11'" },
+ { name = "tenacity", marker = "python_full_version >= '3.11'" },
+ { name = "truststore", marker = "python_full_version >= '3.11'" },
+ { name = "uuid-utils", marker = "python_full_version >= '3.11'" },
+ { name = "uvicorn", marker = "python_full_version >= '3.11'" },
+ { name = "watchfiles", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/52/649481d27ced9b982166b16dcbf57bf024712830186258bf1d876b7e9361/langgraph_api-0.7.8.tar.gz", hash = "sha256:a399474c783c399d9cc503f5cde34fe2886ba468b59c4c629cfd8e4f05926f59", size = 466146, upload-time = "2026-01-23T00:11:47.204Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/23/a0/0bb4a47529f4e1a53b690897d756e3da0d3d1ce6990238f71085d1ce3a50/langgraph_api-0.7.8-py3-none-any.whl", hash = "sha256:2f0bfc13674bc3347321064268ecc951c35518de054eedcc9b426ecb269613ae", size = 369565, upload-time = "2026-01-23T00:11:45.729Z" },
+]
+
+[[package]]
+name = "langgraph-checkpoint"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "langchain-core", marker = "python_full_version >= '3.11'" },
+ { name = "ormsgpack", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/76/55a18c59dedf39688d72c4b06af73a5e3ea0d1a01bc867b88fbf0659f203/langgraph_checkpoint-4.0.0.tar.gz", hash = "sha256:814d1bd050fac029476558d8e68d87bce9009a0262d04a2c14b918255954a624", size = 137320, upload-time = "2026-01-12T20:30:26.38Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4a/de/ddd53b7032e623f3c7bcdab2b44e8bf635e468f62e10e5ff1946f62c9356/langgraph_checkpoint-4.0.0-py3-none-any.whl", hash = "sha256:3fa9b2635a7c5ac28b338f631abf6a030c3b508b7b9ce17c22611513b589c784", size = 46329, upload-time = "2026-01-12T20:30:25.2Z" },
+]
+
+[[package]]
+name = "langgraph-cli"
+source = { editable = "." }
+dependencies = [
+ { name = "click" },
+ { name = "langgraph-sdk", marker = "python_full_version >= '3.11'" },
+]
+
+[package.optional-dependencies]
+inmem = [
+ { name = "langgraph-api", marker = "python_full_version >= '3.11'" },
+ { name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11'" },
+ { name = "python-dotenv" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "codespell" },
+ { name = "hatch" },
+ { name = "msgspec" },
+ { name = "mypy" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watch" },
+ { name = "ruff" },
+]
+lint = [
+ { name = "codespell" },
+ { name = "mypy" },
+ { name = "ruff" },
+]
+test = [
+ { name = "msgspec" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watch" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "click", specifier = ">=8.1.7" },
+ { name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.5.35,<0.8.0" },
+ { name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.7" },
+ { name = "langgraph-sdk", marker = "python_full_version >= '3.11'", specifier = ">=0.1.0" },
+ { name = "python-dotenv", marker = "extra == 'inmem'", specifier = ">=0.8.0" },
+]
+provides-extras = ["inmem"]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "codespell" },
+ { name = "hatch", specifier = ">=1.16.2" },
+ { name = "msgspec" },
+ { name = "mypy" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watch" },
+ { name = "ruff" },
+]
+lint = [
+ { name = "codespell" },
+ { name = "mypy" },
+ { name = "ruff" },
+]
+test = [
+ { name = "msgspec" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "pytest-watch" },
+]
+
+[[package]]
+name = "langgraph-prebuilt"
+version = "1.0.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "langchain-core", marker = "python_full_version >= '3.11'" },
+ { name = "langgraph-checkpoint", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a7/59/711aecd1a50999456850dc328f3cad72b4372d8218838d8d5326f80cb76f/langgraph_prebuilt-1.0.7.tar.gz", hash = "sha256:38e097e06de810de4d0e028ffc0e432bb56d1fb417620fb1dfdc76c5e03e4bf9", size = 163692, upload-time = "2026-01-22T16:45:22.801Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/47/49/5e37abb3f38a17a3487634abc2a5da87c208cc1d14577eb8d7184b25c886/langgraph_prebuilt-1.0.7-py3-none-any.whl", hash = "sha256:e14923516504405bb5edc3977085bc9622c35476b50c1808544490e13871fe7c", size = 35324, upload-time = "2026-01-22T16:45:21.784Z" },
+]
+
+[[package]]
+name = "langgraph-runtime-inmem"
+version = "0.23.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "blockbuster", marker = "python_full_version >= '3.11'" },
+ { name = "langgraph", marker = "python_full_version >= '3.11'" },
+ { name = "langgraph-checkpoint", marker = "python_full_version >= '3.11'" },
+ { name = "sse-starlette", marker = "python_full_version >= '3.11'" },
+ { name = "starlette", marker = "python_full_version >= '3.11'" },
+ { name = "structlog", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d0/25/f8ce026c9f56678ba3a0ee07456b60643be9ea46860b1b0268fe064bb1a8/langgraph_runtime_inmem-0.23.1.tar.gz", hash = "sha256:f7854bb2742dd797ae8d19a51d3b72d6218b51cb961af4fe9b437bb50710890b", size = 104659, upload-time = "2026-01-22T18:47:47.004Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/1e/db6c2a5189793b49c482449cff38ff60df757e7081c209678df4e4693d57/langgraph_runtime_inmem-0.23.1-py3-none-any.whl", hash = "sha256:79763b741d86643b28d6ea4b9b11f3189a64d1e233127339b458efd2f6f647f7", size = 39017, upload-time = "2026-01-22T18:47:45.567Z" },
+]
+
+[[package]]
+name = "langgraph-sdk"
+version = "0.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "httpx", marker = "python_full_version >= '3.11'" },
+ { name = "orjson", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c3/0f/ed0634c222eed48a31ba48eab6881f94ad690d65e44fe7ca838240a260c1/langgraph_sdk-0.3.3.tar.gz", hash = "sha256:c34c3dce3b6848755eb61f0c94369d1ba04aceeb1b76015db1ea7362c544fb26", size = 130589, upload-time = "2026-01-13T00:30:43.894Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6e/be/4ad511bacfdd854afb12974f407cb30010dceb982dc20c55491867b34526/langgraph_sdk-0.3.3-py3-none-any.whl", hash = "sha256:a52ebaf09d91143e55378bb2d0b033ed98f57f48c9ad35c8f81493b88705fc7b", size = 67021, upload-time = "2026-01-13T00:30:42.264Z" },
+]
+
+[[package]]
+name = "langsmith"
+version = "0.6.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "httpx", marker = "python_full_version >= '3.11'" },
+ { name = "orjson", marker = "python_full_version >= '3.11' and platform_python_implementation != 'PyPy'" },
+ { name = "packaging", marker = "python_full_version >= '3.11'" },
+ { name = "pydantic", marker = "python_full_version >= '3.11'" },
+ { name = "requests", marker = "python_full_version >= '3.11'" },
+ { name = "requests-toolbelt", marker = "python_full_version >= '3.11'" },
+ { name = "uuid-utils", marker = "python_full_version >= '3.11'" },
+ { name = "zstandard", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e7/85/9c7933052a997da1b85bc5c774f3865e9b1da1c8d71541ea133178b13229/langsmith-0.6.4.tar.gz", hash = "sha256:36f7223a01c218079fbb17da5e536ebbaf5c1468c028abe070aa3ae59bc99ec8", size = 919964, upload-time = "2026-01-15T20:02:28.873Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/66/0f/09a6637a7ba777eb307b7c80852d9ee26438e2bdafbad6fcc849ff9d9192/langsmith-0.6.4-py3-none-any.whl", hash = "sha256:ac4835860160be371042c7adbba3cb267bcf8d96a5ea976c33a8a4acad6c5486", size = 283503, upload-time = "2026-01-15T20:02:26.662Z" },
+]
+
+[package.optional-dependencies]
+otel = [
+ { name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
+ { name = "opentelemetry-exporter-otlp-proto-http", marker = "python_full_version >= '3.11'" },
+ { name = "opentelemetry-sdk", marker = "python_full_version >= '3.11'" },
+]
+
+[[package]]
+name = "librt"
+version = "0.7.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" },
+ { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" },
+ { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" },
+ { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" },
+ { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" },
+ { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" },
+ { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" },
+ { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" },
+ { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" },
+ { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" },
+ { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" },
+ { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" },
+ { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" },
+ { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" },
+ { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" },
+ { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" },
+ { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" },
+ { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" },
+ { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" },
+ { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" },
+ { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" },
+ { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" },
+ { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" },
+ { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
+[[package]]
+name = "more-itertools"
+version = "10.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" },
+]
+
+[[package]]
+name = "msgspec"
+version = "0.20.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ea/9c/bfbd12955a49180cbd234c5d29ec6f74fe641698f0cd9df154a854fc8a15/msgspec-0.20.0.tar.gz", hash = "sha256:692349e588fde322875f8d3025ac01689fead5901e7fb18d6870a44519d62a29", size = 317862, upload-time = "2025-11-24T03:56:28.934Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e3/5e/151883ba2047cca9db8ed2f86186b054ad200bc231352df15b0c1dd75b1f/msgspec-0.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:23a6ec2a3b5038c233b04740a545856a068bc5cb8db184ff493a58e08c994fbf", size = 195191, upload-time = "2025-11-24T03:55:08.549Z" },
+ { url = "https://files.pythonhosted.org/packages/50/88/a795647672f547c983eff0823b82aaa35db922c767e1b3693e2dcf96678d/msgspec-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cde2c41ed3eaaef6146365cb0d69580078a19f974c6cb8165cc5dcd5734f573e", size = 188513, upload-time = "2025-11-24T03:55:10.008Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/91/eb0abb0e0de142066cebfe546dc9140c5972ea824aa6ff507ad0b6a126ac/msgspec-0.20.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5da0daa782f95d364f0d95962faed01e218732aa1aa6cad56b25a5d2092e75a4", size = 216370, upload-time = "2025-11-24T03:55:11.566Z" },
+ { url = "https://files.pythonhosted.org/packages/15/2a/48e41d9ef0a24b1c6e67cbd94a676799e0561bfbc163be1aaaff5ca853f5/msgspec-0.20.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9369d5266144bef91be2940a3821e03e51a93c9080fde3ef72728c3f0a3a8bb7", size = 222653, upload-time = "2025-11-24T03:55:13.159Z" },
+ { url = "https://files.pythonhosted.org/packages/90/c9/14b825df203d980f82a623450d5f39e7f7a09e6e256c52b498ea8f29d923/msgspec-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90fb865b306ca92c03964a5f3d0cd9eb1adda14f7e5ac7943efd159719ea9f10", size = 222337, upload-time = "2025-11-24T03:55:14.777Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/d7/39a5c3ddd294f587d6fb8efccc8361b6aa5089974015054071e665c9d24b/msgspec-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e8112cd48b67dfc0cfa49fc812b6ce7eb37499e1d95b9575061683f3428975d3", size = 225565, upload-time = "2025-11-24T03:55:16.4Z" },
+ { url = "https://files.pythonhosted.org/packages/98/bd/5db3c14d675ee12842afb9b70c94c64f2c873f31198c46cbfcd7dffafab0/msgspec-0.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:666b966d503df5dc27287675f525a56b6e66a2b8e8ccd2877b0c01328f19ae6c", size = 188412, upload-time = "2025-11-24T03:55:17.747Z" },
+ { url = "https://files.pythonhosted.org/packages/76/c7/06cc218bc0c86f0c6c6f34f7eeea6cfb8b835070e8031e3b0ef00f6c7c69/msgspec-0.20.0-cp310-cp310-win_arm64.whl", hash = "sha256:099e3e85cd5b238f2669621be65f0728169b8c7cb7ab07f6137b02dc7feea781", size = 173951, upload-time = "2025-11-24T03:55:19.335Z" },
+ { url = "https://files.pythonhosted.org/packages/03/59/fdcb3af72f750a8de2bcf39d62ada70b5eb17b06d7f63860e0a679cb656b/msgspec-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:09e0efbf1ac641fedb1d5496c59507c2f0dc62a052189ee62c763e0aae217520", size = 193345, upload-time = "2025-11-24T03:55:20.613Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/15/3c225610da9f02505d37d69a77f4a2e7daae2a125f99d638df211ba84e59/msgspec-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23ee3787142e48f5ee746b2909ce1b76e2949fbe0f97f9f6e70879f06c218b54", size = 186867, upload-time = "2025-11-24T03:55:22.4Z" },
+ { url = "https://files.pythonhosted.org/packages/81/36/13ab0c547e283bf172f45491edfdea0e2cecb26ae61e3a7b1ae6058b326d/msgspec-0.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81f4ac6f0363407ac0465eff5c7d4d18f26870e00674f8fcb336d898a1e36854", size = 215351, upload-time = "2025-11-24T03:55:23.958Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/96/5c095b940de3aa6b43a71ec76275ac3537b21bd45c7499b5a17a429110fa/msgspec-0.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb4d873f24ae18cd1334f4e37a178ed46c9d186437733351267e0a269bdf7e53", size = 219896, upload-time = "2025-11-24T03:55:25.356Z" },
+ { url = "https://files.pythonhosted.org/packages/98/7a/81a7b5f01af300761087b114dafa20fb97aed7184d33aab64d48874eb187/msgspec-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b92b8334427b8393b520c24ff53b70f326f79acf5f74adb94fd361bcff8a1d4e", size = 220389, upload-time = "2025-11-24T03:55:26.99Z" },
+ { url = "https://files.pythonhosted.org/packages/70/c0/3d0cce27db9a9912421273d49eab79ce01ecd2fed1a2f1b74af9b445f33c/msgspec-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:562c44b047c05cc0384e006fae7a5e715740215c799429e0d7e3e5adf324285a", size = 223348, upload-time = "2025-11-24T03:55:28.311Z" },
+ { url = "https://files.pythonhosted.org/packages/89/5e/406b7d578926b68790e390d83a1165a9bfc2d95612a1a9c1c4d5c72ea815/msgspec-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:d1dcc93a3ce3d3195985bfff18a48274d0b5ffbc96fa1c5b89da6f0d9af81b29", size = 188713, upload-time = "2025-11-24T03:55:29.553Z" },
+ { url = "https://files.pythonhosted.org/packages/47/87/14fe2316624ceedf76a9e94d714d194cbcb699720b210ff189f89ca4efd7/msgspec-0.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:aa387aa330d2e4bd69995f66ea8fdc87099ddeedf6fdb232993c6a67711e7520", size = 174229, upload-time = "2025-11-24T03:55:31.107Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/6f/1e25eee957e58e3afb2a44b94fa95e06cebc4c236193ed0de3012fff1e19/msgspec-0.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2aba22e2e302e9231e85edc24f27ba1f524d43c223ef5765bd8624c7df9ec0a5", size = 196391, upload-time = "2025-11-24T03:55:32.677Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/ee/af51d090ada641d4b264992a486435ba3ef5b5634bc27e6eb002f71cef7d/msgspec-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:716284f898ab2547fedd72a93bb940375de9fbfe77538f05779632dc34afdfde", size = 188644, upload-time = "2025-11-24T03:55:33.934Z" },
+ { url = "https://files.pythonhosted.org/packages/49/d6/9709ee093b7742362c2934bfb1bbe791a1e09bed3ea5d8a18ce552fbfd73/msgspec-0.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:558ed73315efa51b1538fa8f1d3b22c8c5ff6d9a2a62eff87d25829b94fc5054", size = 218852, upload-time = "2025-11-24T03:55:35.575Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/a2/488517a43ccf5a4b6b6eca6dd4ede0bd82b043d1539dd6bb908a19f8efd3/msgspec-0.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:509ac1362a1d53aa66798c9b9fd76872d7faa30fcf89b2fba3bcbfd559d56eb0", size = 224937, upload-time = "2025-11-24T03:55:36.859Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/e8/49b832808aa23b85d4f090d1d2e48a4e3834871415031ed7c5fe48723156/msgspec-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1353c2c93423602e7dea1aa4c92f3391fdfc25ff40e0bacf81d34dbc68adb870", size = 222858, upload-time = "2025-11-24T03:55:38.187Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/56/1dc2fa53685dca9c3f243a6cbecd34e856858354e455b77f47ebd76cf5bf/msgspec-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb33b5eb5adb3c33d749684471c6a165468395d7aa02d8867c15103b81e1da3e", size = 227248, upload-time = "2025-11-24T03:55:39.496Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/51/aba940212c23b32eedce752896205912c2668472ed5b205fc33da28a6509/msgspec-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb1d934e435dd3a2b8cf4bbf47a8757100b4a1cfdc2afdf227541199885cdacb", size = 190024, upload-time = "2025-11-24T03:55:40.829Z" },
+ { url = "https://files.pythonhosted.org/packages/41/ad/3b9f259d94f183daa9764fef33fdc7010f7ecffc29af977044fa47440a83/msgspec-0.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:00648b1e19cf01b2be45444ba9dc961bd4c056ffb15706651e64e5d6ec6197b7", size = 175390, upload-time = "2025-11-24T03:55:42.05Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/d1/b902d38b6e5ba3bdddbec469bba388d647f960aeed7b5b3623a8debe8a76/msgspec-0.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c1ff8db03be7598b50dd4b4a478d6fe93faae3bd54f4f17aa004d0e46c14c46", size = 196463, upload-time = "2025-11-24T03:55:43.405Z" },
+ { url = "https://files.pythonhosted.org/packages/57/b6/eff0305961a1d9447ec2b02f8c73c8946f22564d302a504185b730c9a761/msgspec-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f6532369ece217fd37c5ebcfd7e981f2615628c21121b7b2df9d3adcf2fd69b8", size = 188650, upload-time = "2025-11-24T03:55:44.761Z" },
+ { url = "https://files.pythonhosted.org/packages/99/93/f2ec1ae1de51d3fdee998a1ede6b2c089453a2ee82b5c1b361ed9095064a/msgspec-0.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9a1697da2f85a751ac3cc6a97fceb8e937fc670947183fb2268edaf4016d1ee", size = 218834, upload-time = "2025-11-24T03:55:46.441Z" },
+ { url = "https://files.pythonhosted.org/packages/28/83/36557b04cfdc317ed8a525c4993b23e43a8fbcddaddd78619112ca07138c/msgspec-0.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fac7e9c92eddcd24c19d9e5f6249760941485dff97802461ae7c995a2450111", size = 224917, upload-time = "2025-11-24T03:55:48.06Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/56/362037a1ed5be0b88aced59272442c4b40065c659700f4b195a7f4d0ac88/msgspec-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f953a66f2a3eb8d5ea64768445e2bb301d97609db052628c3e1bcb7d87192a9f", size = 222821, upload-time = "2025-11-24T03:55:49.388Z" },
+ { url = "https://files.pythonhosted.org/packages/92/75/fa2370ec341cedf663731ab7042e177b3742645c5dd4f64dc96bd9f18a6b/msgspec-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:247af0313ae64a066d3aea7ba98840f6681ccbf5c90ba9c7d17f3e39dbba679c", size = 227227, upload-time = "2025-11-24T03:55:51.125Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/25/5e8080fe0117f799b1b68008dc29a65862077296b92550632de015128579/msgspec-0.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:67d5e4dfad52832017018d30a462604c80561aa62a9d548fc2bd4e430b66a352", size = 189966, upload-time = "2025-11-24T03:55:52.458Z" },
+ { url = "https://files.pythonhosted.org/packages/79/b6/63363422153937d40e1cb349c5081338401f8529a5a4e216865decd981bf/msgspec-0.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:91a52578226708b63a9a13de287b1ec3ed1123e4a088b198143860c087770458", size = 175378, upload-time = "2025-11-24T03:55:53.721Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/18/62dc13ab0260c7d741dda8dc7f481495b93ac9168cd887dda5929880eef8/msgspec-0.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:eead16538db1b3f7ec6e3ed1f6f7c5dec67e90f76e76b610e1ffb5671815633a", size = 196407, upload-time = "2025-11-24T03:55:55.001Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/1d/b9949e4ad6953e9f9a142c7997b2f7390c81e03e93570c7c33caf65d27e1/msgspec-0.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:703c3bb47bf47801627fb1438f106adbfa2998fe586696d1324586a375fca238", size = 188889, upload-time = "2025-11-24T03:55:56.311Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/19/f8bb2dc0f1bfe46cc7d2b6b61c5e9b5a46c62298e8f4d03bbe499c926180/msgspec-0.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6cdb227dc585fb109305cee0fd304c2896f02af93ecf50a9c84ee54ee67dbb42", size = 219691, upload-time = "2025-11-24T03:55:57.908Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/8e/6b17e43f6eb9369d9858ee32c97959fcd515628a1df376af96c11606cf70/msgspec-0.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27d35044dd8818ac1bd0fedb2feb4fbdff4e3508dd7c5d14316a12a2d96a0de0", size = 224918, upload-time = "2025-11-24T03:55:59.322Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/db/0e833a177db1a4484797adba7f429d4242585980b90882cc38709e1b62df/msgspec-0.20.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4296393a29ee42dd25947981c65506fd4ad39beaf816f614146fa0c5a6c91ae", size = 223436, upload-time = "2025-11-24T03:56:00.716Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/30/d2ee787f4c918fd2b123441d49a7707ae9015e0e8e1ab51aa7967a97b90e/msgspec-0.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:205fbdadd0d8d861d71c8f3399fe1a82a2caf4467bc8ff9a626df34c12176980", size = 227190, upload-time = "2025-11-24T03:56:02.371Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/37/9c4b58ff11d890d788e700b827db2366f4d11b3313bf136780da7017278b/msgspec-0.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:7dfebc94fe7d3feec6bc6c9df4f7e9eccc1160bb5b811fbf3e3a56899e398a6b", size = 193950, upload-time = "2025-11-24T03:56:03.668Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/4e/cab707bf2fa57408e2934e5197fc3560079db34a1e3cd2675ff2e47e07de/msgspec-0.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:2ad6ae36e4a602b24b4bf4eaf8ab5a441fec03e1f1b5931beca8ebda68f53fc0", size = 179018, upload-time = "2025-11-24T03:56:05.038Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/06/3da3fc9aaa55618a8f43eb9052453cfe01f82930bca3af8cea63a89f3a11/msgspec-0.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f84703e0e6ef025663dd1de828ca028774797b8155e070e795c548f76dde65d5", size = 200389, upload-time = "2025-11-24T03:56:06.375Z" },
+ { url = "https://files.pythonhosted.org/packages/83/3b/cc4270a5ceab40dfe1d1745856951b0a24fd16ac8539a66ed3004a60c91e/msgspec-0.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c83fc24dd09cf1275934ff300e3951b3adc5573f0657a643515cc16c7dee131", size = 193198, upload-time = "2025-11-24T03:56:07.742Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/ae/4c7905ac53830c8e3c06fdd60e3cdcfedc0bbc993872d1549b84ea21a1bd/msgspec-0.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f13ccb1c335a124e80c4562573b9b90f01ea9521a1a87f7576c2e281d547f56", size = 225973, upload-time = "2025-11-24T03:56:09.18Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/da/032abac1de4d0678d99eaeadb1323bd9d247f4711c012404ba77ed6f15ca/msgspec-0.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17c2b5ca19f19306fc83c96d85e606d2cc107e0caeea85066b5389f664e04846", size = 229509, upload-time = "2025-11-24T03:56:10.898Z" },
+ { url = "https://files.pythonhosted.org/packages/69/52/fdc7bdb7057a166f309e0b44929e584319e625aaba4771b60912a9321ccd/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d931709355edabf66c2dd1a756b2d658593e79882bc81aae5964969d5a291b63", size = 230434, upload-time = "2025-11-24T03:56:12.48Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/fe/1dfd5f512b26b53043884e4f34710c73e294e7cc54278c3fe28380e42c37/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:565f915d2e540e8a0c93a01ff67f50aebe1f7e22798c6a25873f9fda8d1325f8", size = 231758, upload-time = "2025-11-24T03:56:13.765Z" },
+ { url = "https://files.pythonhosted.org/packages/97/f6/9ba7121b8e0c4e0beee49575d1dbc804e2e72467692f0428cf39ceba1ea5/msgspec-0.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:726f3e6c3c323f283f6021ebb6c8ccf58d7cd7baa67b93d73bfbe9a15c34ab8d", size = 206540, upload-time = "2025-11-24T03:56:15.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/3e/c5187de84bb2c2ca334ab163fcacf19a23ebb1d876c837f81a1b324a15bf/msgspec-0.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:93f23528edc51d9f686808a361728e903d6f2be55c901d6f5c92e44c6d546bfc", size = 183011, upload-time = "2025-11-24T03:56:16.442Z" },
+]
+
+[[package]]
+name = "mypy"
+version = "1.19.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "mypy-extensions" },
+ { name = "pathspec" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" },
+ { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" },
+ { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" },
+ { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
+ { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
+ { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" },
+ { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
+ { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
+ { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
+ { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
+ { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" },
+ { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" },
+ { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "opentelemetry-api"
+version = "1.39.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata", marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" },
+]
+
+[[package]]
+name = "opentelemetry-exporter-otlp-proto-common"
+version = "1.39.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-proto", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" },
+]
+
+[[package]]
+name = "opentelemetry-exporter-otlp-proto-http"
+version = "1.39.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "googleapis-common-protos", marker = "python_full_version >= '3.11'" },
+ { name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
+ { name = "opentelemetry-exporter-otlp-proto-common", marker = "python_full_version >= '3.11'" },
+ { name = "opentelemetry-proto", marker = "python_full_version >= '3.11'" },
+ { name = "opentelemetry-sdk", marker = "python_full_version >= '3.11'" },
+ { name = "requests", marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" },
+]
+
+[[package]]
+name = "opentelemetry-proto"
+version = "1.39.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "protobuf", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" },
+]
+
+[[package]]
+name = "opentelemetry-sdk"
+version = "1.39.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
+ { name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" },
+]
+
+[[package]]
+name = "opentelemetry-semantic-conventions"
+version = "0.60b1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" },
+]
+
+[[package]]
+name = "orjson"
+version = "3.11.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/19/b22cf9dad4db20c8737041046054cbd4f38bb5a2d0e4bb60487832ce3d76/orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1", size = 245719, upload-time = "2025-12-06T15:53:43.877Z" },
+ { url = "https://files.pythonhosted.org/packages/03/2e/b136dd6bf30ef5143fbe76a4c142828b55ccc618be490201e9073ad954a1/orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870", size = 132467, upload-time = "2025-12-06T15:53:45.379Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/fc/ae99bfc1e1887d20a0268f0e2686eb5b13d0ea7bbe01de2b566febcd2130/orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09", size = 130702, upload-time = "2025-12-06T15:53:46.659Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/43/ef7912144097765997170aca59249725c3ab8ef6079f93f9d708dd058df5/orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd", size = 135907, upload-time = "2025-12-06T15:53:48.487Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/da/24d50e2d7f4092ddd4d784e37a3fa41f22ce8ed97abc9edd222901a96e74/orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac", size = 139935, upload-time = "2025-12-06T15:53:49.88Z" },
+ { url = "https://files.pythonhosted.org/packages/02/4a/b4cb6fcbfff5b95a3a019a8648255a0fac9b221fbf6b6e72be8df2361feb/orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e", size = 137541, upload-time = "2025-12-06T15:53:51.226Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/99/a11bd129f18c2377c27b2846a9d9be04acec981f770d711ba0aaea563984/orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f", size = 139031, upload-time = "2025-12-06T15:53:52.309Z" },
+ { url = "https://files.pythonhosted.org/packages/64/29/d7b77d7911574733a036bb3e8ad7053ceb2b7d6ea42208b9dbc55b23b9ed/orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18", size = 141622, upload-time = "2025-12-06T15:53:53.606Z" },
+ { url = "https://files.pythonhosted.org/packages/93/41/332db96c1de76b2feda4f453e91c27202cd092835936ce2b70828212f726/orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a", size = 413800, upload-time = "2025-12-06T15:53:54.866Z" },
+ { url = "https://files.pythonhosted.org/packages/76/e1/5a0d148dd1f89ad2f9651df67835b209ab7fcb1118658cf353425d7563e9/orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7", size = 151198, upload-time = "2025-12-06T15:53:56.383Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/96/8db67430d317a01ae5cf7971914f6775affdcfe99f5bff9ef3da32492ecc/orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401", size = 141984, upload-time = "2025-12-06T15:53:57.746Z" },
+ { url = "https://files.pythonhosted.org/packages/71/49/40d21e1aa1ac569e521069228bb29c9b5a350344ccf922a0227d93c2ed44/orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8", size = 135272, upload-time = "2025-12-06T15:53:59.769Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/7e/d0e31e78be0c100e08be64f48d2850b23bcb4d4c70d114f4e43b39f6895a/orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167", size = 133360, upload-time = "2025-12-06T15:54:01.25Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" },
+ { url = "https://files.pythonhosted.org/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c6/0a8caff96f4503f4f7dd44e40e90f4d14acf80d3b7a97cb88747bb712d3e/orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7", size = 130516, upload-time = "2025-12-06T15:54:06.274Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/63/43d4dc9bd9954bff7052f700fdb501067f6fb134a003ddcea2a0bb3854ed/orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd", size = 135695, upload-time = "2025-12-06T15:54:07.702Z" },
+ { url = "https://files.pythonhosted.org/packages/87/6f/27e2e76d110919cb7fcb72b26166ee676480a701bcf8fc53ac5d0edce32f/orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9", size = 139664, upload-time = "2025-12-06T15:54:08.828Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/f8/5966153a5f1be49b5fbb8ca619a529fde7bc71aa0a376f2bb83fed248bcd/orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef", size = 137289, upload-time = "2025-12-06T15:54:09.898Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/34/8acb12ff0299385c8bbcbb19fbe40030f23f15a6de57a9c587ebf71483fb/orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9", size = 138784, upload-time = "2025-12-06T15:54:11.022Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/27/910421ea6e34a527f73d8f4ee7bdffa48357ff79c7b8d6eb6f7b82dd1176/orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125", size = 141322, upload-time = "2025-12-06T15:54:12.427Z" },
+ { url = "https://files.pythonhosted.org/packages/87/a3/4b703edd1a05555d4bb1753d6ce44e1a05b7a6d7c164d5b332c795c63d70/orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814", size = 413612, upload-time = "2025-12-06T15:54:13.858Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/36/034177f11d7eeea16d3d2c42a1883b0373978e08bc9dad387f5074c786d8/orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5", size = 150993, upload-time = "2025-12-06T15:54:15.189Z" },
+ { url = "https://files.pythonhosted.org/packages/44/2f/ea8b24ee046a50a7d141c0227c4496b1180b215e728e3b640684f0ea448d/orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880", size = 141774, upload-time = "2025-12-06T15:54:16.451Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/12/cc440554bf8200eb23348a5744a575a342497b65261cd65ef3b28332510a/orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d", size = 135109, upload-time = "2025-12-06T15:54:17.73Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/83/e0c5aa06ba73a6760134b169f11fb970caa1525fa4461f94d76e692299d9/orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1", size = 133193, upload-time = "2025-12-06T15:54:19.426Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/35/5b77eaebc60d735e832c5b1a20b155667645d123f09d471db0a78280fb49/orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c", size = 126830, upload-time = "2025-12-06T15:54:20.836Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" },
+ { url = "https://files.pythonhosted.org/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" },
+ { url = "https://files.pythonhosted.org/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" },
+ { url = "https://files.pythonhosted.org/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" },
+ { url = "https://files.pythonhosted.org/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" },
+ { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" },
+ { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" },
+ { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" },
+ { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" },
+ { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" },
+ { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" },
+ { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" },
+ { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" },
+ { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" },
+ { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" },
+ { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" },
+ { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" },
+ { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" },
+ { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" },
+]
+
+[[package]]
+name = "ormsgpack"
+version = "1.12.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/93/fa/a91f70829ebccf6387c4946e0a1a109f6ba0d6a28d65f628bedfad94b890/ormsgpack-1.12.2-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c1429217f8f4d7fcb053523bbbac6bed5e981af0b85ba616e6df7cce53c19657", size = 378262, upload-time = "2026-01-18T20:55:22.284Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/62/3698a9a0c487252b5c6a91926e5654e79e665708ea61f67a8bdeceb022bf/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f13034dc6c84a6280c6c33db7ac420253852ea233fc3ee27c8875f8dd651163", size = 203034, upload-time = "2026-01-18T20:55:53.324Z" },
+ { url = "https://files.pythonhosted.org/packages/66/3a/f716f64edc4aec2744e817660b317e2f9bb8de372338a95a96198efa1ac1/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59f5da97000c12bc2d50e988bdc8576b21f6ab4e608489879d35b2c07a8ab51a", size = 210538, upload-time = "2026-01-18T20:55:20.097Z" },
+ { url = "https://files.pythonhosted.org/packages/72/30/a436be9ce27d693d4e19fa94900028067133779f09fc45776db3f689c822/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e4459c3f27066beadb2b81ea48a076a417aafffff7df1d3c11c519190ed44f2", size = 212401, upload-time = "2026-01-18T20:55:46.447Z" },
+ { url = "https://files.pythonhosted.org/packages/10/c5/cde98300fd33fee84ca71de4751b19aeeca675f0cf3c0ec4b043f40f3b76/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a1c460655d7288407ffa09065e322a7231997c0d62ce914bf3a96ad2dc6dedd", size = 387080, upload-time = "2026-01-18T20:56:00.884Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/31/30bf445ef827546747c10889dd254b3d84f92b591300efe4979d792f4c41/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:458e4568be13d311ef7d8877275e7ccbe06c0e01b39baaac874caaa0f46d826c", size = 482346, upload-time = "2026-01-18T20:55:39.831Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/f5/e1745ddf4fa246c921b5ca253636c4c700ff768d78032f79171289159f6e/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8cde5eaa6c6cbc8622db71e4a23de56828e3d876aeb6460ffbcb5b8aff91093b", size = 425178, upload-time = "2026-01-18T20:55:27.106Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/a2/e6532ed7716aed03dede8df2d0d0d4150710c2122647d94b474147ccd891/ormsgpack-1.12.2-cp310-cp310-win_amd64.whl", hash = "sha256:dc7a33be14c347893edbb1ceda89afbf14c467d593a5ee92c11de4f1666b4d4f", size = 117183, upload-time = "2026-01-18T20:55:55.52Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9", size = 378266, upload-time = "2026-01-18T20:55:59.876Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a", size = 203035, upload-time = "2026-01-18T20:55:30.59Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5", size = 210539, upload-time = "2026-01-18T20:55:48.569Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/33/543627f323ff3c73091f51d6a20db28a1a33531af30873ea90c5ac95a9b5/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181", size = 212401, upload-time = "2026-01-18T20:56:10.101Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/5d/f70e2c3da414f46186659d24745483757bcc9adccb481a6eb93e2b729301/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b", size = 387082, upload-time = "2026-01-18T20:56:12.047Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/d6/06e8dc920c7903e051f30934d874d4afccc9bb1c09dcaf0bc03a7de4b343/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92", size = 482346, upload-time = "2026-01-18T20:56:05.152Z" },
+ { url = "https://files.pythonhosted.org/packages/66/c4/f337ac0905eed9c393ef990c54565cd33644918e0a8031fe48c098c71dbf/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a", size = 425181, upload-time = "2026-01-18T20:55:37.83Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/6d5758fabef3babdf4bbbc453738cc7de9cd3334e4c38dd5737e27b85653/ormsgpack-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c", size = 117182, upload-time = "2026-01-18T20:55:31.472Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/57/17a15549233c37e7fd054c48fe9207492e06b026dbd872b826a0b5f833b6/ormsgpack-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd", size = 111464, upload-time = "2026-01-18T20:55:38.811Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" },
+ { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" },
+ { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" },
+ { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" },
+ { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" },
+ { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" },
+ { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" },
+ { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" },
+ { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" },
+ { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "1.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" },
+]
+
+[[package]]
+name = "pexpect"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ptyprocess" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "protobuf"
+version = "6.33.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/b8/cda15d9d46d03d4aa3a67cb6bffe05173440ccf86a9541afaf7ac59a1b6b/protobuf-6.33.4.tar.gz", hash = "sha256:dc2e61bca3b10470c1912d166fe0af67bfc20eb55971dcef8dfa48ce14f0ed91", size = 444346, upload-time = "2026-01-12T18:33:40.109Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/be/24ef9f3095bacdf95b458543334d0c4908ccdaee5130420bf064492c325f/protobuf-6.33.4-cp310-abi3-win32.whl", hash = "sha256:918966612c8232fc6c24c78e1cd89784307f5814ad7506c308ee3cf86662850d", size = 425612, upload-time = "2026-01-12T18:33:29.656Z" },
+ { url = "https://files.pythonhosted.org/packages/31/ad/e5693e1974a28869e7cd244302911955c1cebc0161eb32dfa2b25b6e96f0/protobuf-6.33.4-cp310-abi3-win_amd64.whl", hash = "sha256:8f11ffae31ec67fc2554c2ef891dcb561dae9a2a3ed941f9e134c2db06657dbc", size = 436962, upload-time = "2026-01-12T18:33:31.345Z" },
+ { url = "https://files.pythonhosted.org/packages/66/15/6ee23553b6bfd82670207ead921f4d8ef14c107e5e11443b04caeb5ab5ec/protobuf-6.33.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2fe67f6c014c84f655ee06f6f66213f9254b3a8b6bda6cda0ccd4232c73c06f0", size = 427612, upload-time = "2026-01-12T18:33:32.646Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/48/d301907ce6d0db75f959ca74f44b475a9caa8fcba102d098d3c3dd0f2d3f/protobuf-6.33.4-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:757c978f82e74d75cba88eddec479df9b99a42b31193313b75e492c06a51764e", size = 324484, upload-time = "2026-01-12T18:33:33.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/1c/e53078d3f7fe710572ab2dcffd993e1e3b438ae71cfc031b71bae44fcb2d/protobuf-6.33.4-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c7c64f259c618f0bef7bee042075e390debbf9682334be2b67408ec7c1c09ee6", size = 339256, upload-time = "2026-01-12T18:33:35.231Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/8e/971c0edd084914f7ee7c23aa70ba89e8903918adca179319ee94403701d5/protobuf-6.33.4-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:3df850c2f8db9934de4cf8f9152f8dc2558f49f298f37f90c517e8e5c84c30e9", size = 323311, upload-time = "2026-01-12T18:33:36.305Z" },
+ { url = "https://files.pythonhosted.org/packages/75/b1/1dc83c2c661b4c62d56cc081706ee33a4fc2835bd90f965baa2663ef7676/protobuf-6.33.4-py3-none-any.whl", hash = "sha256:1fe3730068fcf2e595816a6c34fe66eeedd37d51d0400b72fabc848811fdc1bc", size = 170532, upload-time = "2026-01-12T18:33:39.199Z" },
+]
+
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types", marker = "python_full_version >= '3.11'" },
+ { name = "pydantic-core", marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version >= '3.11'" },
+ { name = "typing-inspection", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" },
+ { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" },
+ { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" },
+ { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+ { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
+ { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" },
+ { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" },
+ { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pyjwt"
+version = "2.10.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
+]
+
+[[package]]
+name = "pyproject-hooks"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
+ { name = "pytest" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
+]
+
+[[package]]
+name = "pytest-mock"
+version = "3.15.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
+]
+
+[[package]]
+name = "pytest-watch"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama" },
+ { name = "docopt" },
+ { name = "pytest" },
+ { name = "watchdog" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/36/47/ab65fc1d682befc318c439940f81a0de1026048479f732e84fe714cd69c0/pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9", size = 16340, upload-time = "2018-05-20T19:52:16.194Z" }
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
+]
+
+[[package]]
+name = "pywin32-ctypes"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
+ { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
+ { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi", marker = "python_full_version >= '3.11'" },
+ { name = "charset-normalizer", marker = "python_full_version >= '3.11'" },
+ { name = "idna", marker = "python_full_version >= '3.11'" },
+ { name = "urllib3", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+]
+
+[[package]]
+name = "requests-toolbelt"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "requests", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
+]
+
+[[package]]
+name = "rich"
+version = "14.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.14.14"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" },
+ { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" },
+ { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" },
+ { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" },
+ { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" },
+]
+
+[[package]]
+name = "secretstorage"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+ { name = "jeepney" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
+]
+
+[[package]]
+name = "setuptools"
+version = "80.10.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/86/ff/f75651350db3cf2ef767371307eb163f3cc1ac03e16fdf3ac347607f7edb/setuptools-80.10.1.tar.gz", hash = "sha256:bf2e513eb8144c3298a3bd28ab1a5edb739131ec5c22e045ff93cd7f5319703a", size = 1229650, upload-time = "2026-01-21T09:42:03.061Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/76/f963c61683a39084aa575f98089253e1e852a4417cb8a3a8a422923a5246/setuptools-80.10.1-py3-none-any.whl", hash = "sha256:fc30c51cbcb8199a219c12cc9c281b5925a4978d212f84229c909636d9f6984e", size = 1099859, upload-time = "2026-01-21T09:42:00.688Z" },
+]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "2.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio", marker = "python_full_version >= '3.11'" },
+ { name = "starlette", marker = "python_full_version >= '3.11'" },
+ { name = "uvicorn", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/fc/56ab9f116b2133521f532fce8d03194cf04dcac25f583cf3d839be4c0496/sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169", size = 19678, upload-time = "2024-08-01T08:52:50.248Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/52/aa/36b271bc4fa1d2796311ee7c7283a3a1c348bad426d37293609ca4300eef/sse_starlette-2.1.3-py3-none-any.whl", hash = "sha256:8ec846438b4665b9e8c560fcdea6bc8081a3abf7942faa95e5a744999d219772", size = 9383, upload-time = "2024-08-01T08:52:48.659Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.52.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio", marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" },
+]
+
+[[package]]
+name = "structlog"
+version = "25.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ef/52/9ba0f43b686e7f3ddfeaa78ac3af750292662284b3661e91ad5494f21dbc/structlog-25.5.0.tar.gz", hash = "sha256:098522a3bebed9153d4570c6d0288abf80a031dfdb2048d59a49e9dc2190fc98", size = 1460830, upload-time = "2025-10-27T08:28:23.028Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" },
+]
+
+[[package]]
+name = "tenacity"
+version = "9.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
+ { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
+ { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
+ { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
+ { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
+ { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
+ { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
+ { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
+ { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
+ { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
+ { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
+ { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
+ { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
+]
+
+[[package]]
+name = "tomli-w"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" },
+]
+
+[[package]]
+name = "tomlkit"
+version = "0.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" },
+]
+
+[[package]]
+name = "trove-classifiers"
+version = "2026.1.14.14"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/43/7935f8ea93fcb6680bc10a6fdbf534075c198eeead59150dd5ed68449642/trove_classifiers-2026.1.14.14.tar.gz", hash = "sha256:00492545a1402b09d4858605ba190ea33243d361e2b01c9c296ce06b5c3325f3", size = 16997, upload-time = "2026-01-14T14:54:50.526Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl", hash = "sha256:1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d", size = 14197, upload-time = "2026-01-14T14:54:49.067Z" },
+]
+
+[[package]]
+name = "truststore"
+version = "0.10.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
+]
+
+[[package]]
+name = "userpath"
+version = "1.9.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d5/b7/30753098208505d7ff9be5b3a32112fb8a4cb3ddfccbbb7ba9973f2e29ff/userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815", size = 11140, upload-time = "2024-02-29T21:39:08.742Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", size = 9065, upload-time = "2024-02-29T21:39:07.551Z" },
+]
+
+[[package]]
+name = "uuid-utils"
+version = "0.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/7c/3a926e847516e67bc6838634f2e54e24381105b4e80f9338dc35cca0086b/uuid_utils-0.14.0.tar.gz", hash = "sha256:fc5bac21e9933ea6c590433c11aa54aaca599f690c08069e364eb13a12f670b4", size = 22072, upload-time = "2026-01-20T20:37:15.729Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/42/42d003f4a99ddc901eef2fd41acb3694163835e037fb6dde79ad68a72342/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f6695c0bed8b18a904321e115afe73b34444bc8451d0ce3244a1ec3b84deb0e5", size = 601786, upload-time = "2026-01-20T20:37:09.843Z" },
+ { url = "https://files.pythonhosted.org/packages/96/e6/775dfb91f74b18f7207e3201eb31ee666d286579990dc69dd50db2d92813/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4f0a730bbf2d8bb2c11b93e1005e91769f2f533fa1125ed1f00fd15b6fcc732b", size = 303943, upload-time = "2026-01-20T20:37:18.767Z" },
+ { url = "https://files.pythonhosted.org/packages/17/82/ea5f5e85560b08a1f30cdc65f75e76494dc7aba9773f679e7eaa27370229/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ce3fd1a4fdedae618fc3edc8faf91897012469169d600133470f49fd699ed3", size = 340467, upload-time = "2026-01-20T20:37:11.794Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/33/54b06415767f4569882e99b6470c6c8eeb97422686a6d432464f9967fd91/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09ae4a98416a440e78f7d9543d11b11cae4bab538b7ed94ec5da5221481748f2", size = 346333, upload-time = "2026-01-20T20:37:12.818Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/10/a6bce636b8f95e65dc84bf4a58ce8205b8e0a2a300a38cdbc83a3f763d27/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:971e8c26b90d8ae727e7f2ac3ee23e265971d448b3672882f2eb44828b2b8c3e", size = 470859, upload-time = "2026-01-20T20:37:01.512Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/27/84121c51ea72f013f0e03d0886bcdfa96b31c9b83c98300a7bd5cc4fa191/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5cde1fa82804a8f9d2907b7aec2009d440062c63f04abbdb825fce717a5e860", size = 341988, upload-time = "2026-01-20T20:37:22.881Z" },
+ { url = "https://files.pythonhosted.org/packages/90/a4/01c1c7af5e6a44f20b40183e8dac37d6ed83e7dc9e8df85370a15959b804/uuid_utils-0.14.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7343862a2359e0bd48a7f3dfb5105877a1728677818bb694d9f40703264a2db", size = 365784, upload-time = "2026-01-20T20:37:10.808Z" },
+ { url = "https://files.pythonhosted.org/packages/04/f0/65ee43ec617b8b6b1bf2a5aecd56a069a08cca3d9340c1de86024331bde3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c51e4818fdb08ccec12dc7083a01f49507b4608770a0ab22368001685d59381b", size = 523750, upload-time = "2026-01-20T20:37:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/95/d3/6bf503e3f135a5dfe705a65e6f89f19bccd55ac3fb16cb5d3ec5ba5388b8/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:181bbcccb6f93d80a8504b5bd47b311a1c31395139596edbc47b154b0685b533", size = 615818, upload-time = "2026-01-20T20:37:21.816Z" },
+ { url = "https://files.pythonhosted.org/packages/df/6c/99937dd78d07f73bba831c8dc9469dfe4696539eba2fc269ae1b92752f9e/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:5c8ae96101c3524ba8dbf762b6f05e9e9d896544786c503a727c5bf5cb9af1a7", size = 580831, upload-time = "2026-01-20T20:37:19.691Z" },
+ { url = "https://files.pythonhosted.org/packages/44/fa/bbc9e2c25abd09a293b9b097a0d8fc16acd6a92854f0ec080f1ea7ad8bb3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00ac3c6edfdaff7e1eed041f4800ae09a3361287be780d7610a90fdcde9befdc", size = 546333, upload-time = "2026-01-20T20:37:03.117Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/9b/e5e99b324b1b5f0c62882230455786df0bc66f67eff3b452447e703f45d2/uuid_utils-0.14.0-cp39-abi3-win32.whl", hash = "sha256:ec2fd80adf8e0e6589d40699e6f6df94c93edcc16dd999be0438dd007c77b151", size = 177319, upload-time = "2026-01-20T20:37:04.208Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/28/2c7d417ea483b6ff7820c948678fdf2ac98899dc7e43bb15852faa95acaf/uuid_utils-0.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:efe881eb43a5504fad922644cb93d725fd8a6a6d949bd5a4b4b7d1a1587c7fd1", size = 182566, upload-time = "2026-01-20T20:37:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/86/49e4bdda28e962fbd7266684171ee29b3d92019116971d58783e51770745/uuid_utils-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:32b372b8fd4ebd44d3a219e093fe981af4afdeda2994ee7db208ab065cfcd080", size = 182809, upload-time = "2026-01-20T20:37:05.139Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/03/1f1146e32e94d1f260dfabc81e1649102083303fb4ad549775c943425d9a/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:762e8d67992ac4d2454e24a141a1c82142b5bde10409818c62adbe9924ebc86d", size = 587430, upload-time = "2026-01-20T20:37:24.998Z" },
+ { url = "https://files.pythonhosted.org/packages/87/ba/d5a7469362594d885fd9219fe9e851efbe65101d3ef1ef25ea321d7ce841/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:40be5bf0b13aa849d9062abc86c198be6a25ff35316ce0b89fc25f3bac6d525e", size = 298106, upload-time = "2026-01-20T20:37:23.896Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/11/3dafb2a5502586f59fd49e93f5802cd5face82921b3a0f3abb5f357cb879/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:191a90a6f3940d1b7322b6e6cceff4dd533c943659e0a15f788674407856a515", size = 333423, upload-time = "2026-01-20T20:37:17.828Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f2/c8987663f0cdcf4d717a36d85b5db2a5589df0a4e129aa10f16f4380ef48/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4aa4525f4ad82f9d9c842f9a3703f1539c1808affbaec07bb1b842f6b8b96aa5", size = 338659, upload-time = "2026-01-20T20:37:14.286Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/c8/929d81665d83f0b2ffaecb8e66c3091a50f62c7cb5b65e678bd75a96684e/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdbd82ff20147461caefc375551595ecf77ebb384e46267f128aca45a0f2cdfc", size = 467029, upload-time = "2026-01-20T20:37:08.277Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/a0/27d7daa1bfed7163f4ccaf52d7d2f4ad7bb1002a85b45077938b91ee584f/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff57e8a5d540006ce73cf0841a643d445afe78ba12e75ac53a95ca2924a56be", size = 333298, upload-time = "2026-01-20T20:37:07.271Z" },
+ { url = "https://files.pythonhosted.org/packages/63/d4/acad86ce012b42ce18a12f31ee2aa3cbeeb98664f865f05f68c882945913/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fd9112ca96978361201e669729784f26c71fecc9c13a7f8a07162c31bd4d1e2", size = 359217, upload-time = "2026-01-20T20:36:59.687Z" },
+]
+
+[[package]]
+name = "uv"
+version = "0.9.26"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ff/6a/ef4ea19097ecdfd7df6e608f93874536af045c68fd70aa628c667815c458/uv-0.9.26.tar.gz", hash = "sha256:8b7017a01cc48847a7ae26733383a2456dd060fc50d21d58de5ee14f6b6984d7", size = 3790483, upload-time = "2026-01-15T20:51:33.582Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fe/e1/5c0b17833d5e3b51a897957348ff8d937a3cdfc5eea5c4a7075d8d7b9870/uv-0.9.26-py3-none-linux_armv6l.whl", hash = "sha256:7dba609e32b7bd13ef81788d580970c6ff3a8874d942755b442cffa8f25dba57", size = 22638031, upload-time = "2026-01-15T20:51:44.187Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/8b/68ac5825a615a8697e324f52ac0b92feb47a0ec36a63759c5f2931f0c3a0/uv-0.9.26-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b815e3b26eeed00e00f831343daba7a9d99c1506883c189453bb4d215f54faac", size = 21507805, upload-time = "2026-01-15T20:50:42.574Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/a2/664a338aefe009f6e38e47455ee2f64a21da7ad431dbcaf8b45d8b1a2b7a/uv-0.9.26-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1b012e6c4dfe767f818cbb6f47d02c207c9b0c82fee69a5de6d26ffb26a3ef3c", size = 20249791, upload-time = "2026-01-15T20:50:49.835Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/3d/b8186a7dec1346ca4630c674b760517d28bffa813a01965f4b57596bacf3/uv-0.9.26-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:ea296b700d7c4c27acdfd23ffaef2b0ecdd0aa1b58d942c62ee87df3b30f06ac", size = 22039108, upload-time = "2026-01-15T20:51:00.675Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/a9/687fd587e7a3c2c826afe72214fb24b7f07b0d8b0b0300e6a53b554180ea/uv-0.9.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:1ba860d2988efc27e9c19f8537a2f9fa499a8b7ebe4afbe2d3d323d72f9aee61", size = 22174763, upload-time = "2026-01-15T20:50:46.471Z" },
+ { url = "https://files.pythonhosted.org/packages/38/69/7fa03ee7d59e562fca1426436f15a8c107447d41b34e0899e25ee69abfad/uv-0.9.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8610bdfc282a681a0a40b90495a478599aa3484c12503ef79ef42cd271fd80fe", size = 22189861, upload-time = "2026-01-15T20:51:15.618Z" },
+ { url = "https://files.pythonhosted.org/packages/10/2d/4be446a2ec09f3c428632b00a138750af47c76b0b9f987e9a5b52fef0405/uv-0.9.26-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4bf700bd071bd595084b9ee0a8d77c6a0a10ca3773d3771346a2599f306bd9c", size = 23005589, upload-time = "2026-01-15T20:50:57.185Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/16/860990b812136695a63a8da9fb5f819c3cf18ea37dcf5852e0e1b795ca0d/uv-0.9.26-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:89a7beea1c692f76a6f8da13beff3cbb43f7123609e48e03517cc0db5c5de87c", size = 24713505, upload-time = "2026-01-15T20:51:04.366Z" },
+ { url = "https://files.pythonhosted.org/packages/01/43/5d7f360d551e62d8f8bf6624b8fca9895cea49ebe5fce8891232d7ed2321/uv-0.9.26-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:182f5c086c7d03ad447e522b70fa29a0302a70bcfefad4b8cd08496828a0e179", size = 24342500, upload-time = "2026-01-15T20:51:47.863Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/9c/2bae010a189e7d8e5dc555edcfd053b11ce96fad2301b919ba0d9dd23659/uv-0.9.26-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d8c62a501f13425b4b0ce1dd4c6b82f3ce5a5179e2549c55f4bb27cc0eb8ef8", size = 23222578, upload-time = "2026-01-15T20:51:36.85Z" },
+ { url = "https://files.pythonhosted.org/packages/38/16/a07593a040fe6403c36f3b0a99b309f295cbfe19a1074dbadb671d5d4ef7/uv-0.9.26-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7e89798bd3df7dcc4b2b4ac4e2fc11d6b3ff4fe7d764aa3012d664c635e2922", size = 23250201, upload-time = "2026-01-15T20:51:19.117Z" },
+ { url = "https://files.pythonhosted.org/packages/23/a0/45893e15ad3ab842db27c1eb3b8605b9b4023baa5d414e67cfa559a0bff0/uv-0.9.26-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:60a66f1783ec4efc87b7e1f9bd66e8fd2de3e3b30d122b31cb1487f63a3ea8b7", size = 22229160, upload-time = "2026-01-15T20:51:22.931Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/c0/20a597a5c253702a223b5e745cf8c16cd5dd053080f896bb10717b3bedec/uv-0.9.26-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:63c6a1f1187facba1fb45a2fa45396980631a3427ac11b0e3d9aa3ebcf2c73cf", size = 23090730, upload-time = "2026-01-15T20:51:26.611Z" },
+ { url = "https://files.pythonhosted.org/packages/40/c9/744537867d9ab593fea108638b57cca1165a0889cfd989981c942b6de9a5/uv-0.9.26-py3-none-musllinux_1_1_i686.whl", hash = "sha256:c6d8650fbc980ccb348b168266143a9bd4deebc86437537caaf8ff2a39b6ea50", size = 22436632, upload-time = "2026-01-15T20:51:12.045Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/e2/be683e30262f2cf02dcb41b6c32910a6939517d50ec45f502614d239feb7/uv-0.9.26-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:25278f9298aa4dade38241a93d036739b0c87278dcfad1ec1f57e803536bfc49", size = 23480064, upload-time = "2026-01-15T20:50:53.333Z" },
+ { url = "https://files.pythonhosted.org/packages/50/3e/4a7e6bc5db2beac9c4966f212805f1903d37d233f2e160737f0b24780ada/uv-0.9.26-py3-none-win32.whl", hash = "sha256:10d075e0193e3a0e6c54f830731c4cb965d6f4e11956e84a7bed7ed61d42aa27", size = 21000052, upload-time = "2026-01-15T20:51:40.753Z" },
+ { url = "https://files.pythonhosted.org/packages/07/5d/eb80c6eff2a9f7d5cf35ec84fda323b74aa0054145db28baf72d35a7a301/uv-0.9.26-py3-none-win_amd64.whl", hash = "sha256:0315fc321f5644b12118f9928086513363ed9b29d74d99f1539fda1b6b5478ab", size = 23684930, upload-time = "2026-01-15T20:51:08.448Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/9d/3b2631931649b1783f5024796ca8ad2b42a01a829b9ce1202d973cc7bce5/uv-0.9.26-py3-none-win_arm64.whl", hash = "sha256:344ff38749b6cd7b7dfdfb382536f168cafe917ae3a5aa78b7a63746ba2a905b", size = 22158123, upload-time = "2026-01-15T20:51:30.939Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.40.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click", marker = "python_full_version >= '3.11'" },
+ { name = "h11", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" },
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.36.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" },
+]
+
+[[package]]
+name = "watchdog"
+version = "6.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" },
+ { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" },
+ { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" },
+ { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" },
+ { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
+ { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
+ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
+ { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
+ { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
+ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
+ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
+]
+
+[[package]]
+name = "watchfiles"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" },
+ { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" },
+ { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" },
+ { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" },
+ { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" },
+ { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" },
+ { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" },
+ { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" },
+ { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" },
+ { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" },
+ { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" },
+ { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" },
+ { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" },
+ { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" },
+ { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" },
+ { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" },
+ { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" },
+ { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" },
+ { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" },
+ { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" },
+ { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" },
+ { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" },
+ { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" },
+ { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" },
+ { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" },
+ { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" },
+ { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" },
+ { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" },
+ { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" },
+ { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" },
+ { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" },
+ { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" },
+ { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" },
+ { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" },
+ { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" },
+ { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" },
+ { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" },
+]
+
+[[package]]
+name = "xxhash"
+version = "3.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/34/ee/f9f1d656ad168681bb0f6b092372c1e533c4416b8069b1896a175c46e484/xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71", size = 32845, upload-time = "2025-10-02T14:33:51.573Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/b1/93508d9460b292c74a09b83d16750c52a0ead89c51eea9951cb97a60d959/xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d", size = 30807, upload-time = "2025-10-02T14:33:52.964Z" },
+ { url = "https://files.pythonhosted.org/packages/07/55/28c93a3662f2d200c70704efe74aab9640e824f8ce330d8d3943bf7c9b3c/xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8", size = 193786, upload-time = "2025-10-02T14:33:54.272Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/96/fec0be9bb4b8f5d9c57d76380a366f31a1781fb802f76fc7cda6c84893c7/xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058", size = 212830, upload-time = "2025-10-02T14:33:55.706Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/a0/c706845ba77b9611f81fd2e93fad9859346b026e8445e76f8c6fd057cc6d/xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2", size = 211606, upload-time = "2025-10-02T14:33:57.133Z" },
+ { url = "https://files.pythonhosted.org/packages/67/1e/164126a2999e5045f04a69257eea946c0dc3e86541b400d4385d646b53d7/xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc", size = 444872, upload-time = "2025-10-02T14:33:58.446Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/4b/55ab404c56cd70a2cf5ecfe484838865d0fea5627365c6c8ca156bd09c8f/xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc", size = 193217, upload-time = "2025-10-02T14:33:59.724Z" },
+ { url = "https://files.pythonhosted.org/packages/45/e6/52abf06bac316db33aa269091ae7311bd53cfc6f4b120ae77bac1b348091/xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07", size = 210139, upload-time = "2025-10-02T14:34:02.041Z" },
+ { url = "https://files.pythonhosted.org/packages/34/37/db94d490b8691236d356bc249c08819cbcef9273a1a30acf1254ff9ce157/xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4", size = 197669, upload-time = "2025-10-02T14:34:03.664Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/36/c4f219ef4a17a4f7a64ed3569bc2b5a9c8311abdb22249ac96093625b1a4/xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06", size = 210018, upload-time = "2025-10-02T14:34:05.325Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/06/bfac889a374fc2fc439a69223d1750eed2e18a7db8514737ab630534fa08/xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4", size = 413058, upload-time = "2025-10-02T14:34:06.925Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d1/555d8447e0dd32ad0930a249a522bb2e289f0d08b6b16204cfa42c1f5a0c/xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b", size = 190628, upload-time = "2025-10-02T14:34:08.669Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/15/8751330b5186cedc4ed4b597989882ea05e0408b53fa47bcb46a6125bfc6/xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b", size = 30577, upload-time = "2025-10-02T14:34:10.234Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/cc/53f87e8b5871a6eb2ff7e89c48c66093bda2be52315a8161ddc54ea550c4/xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb", size = 31487, upload-time = "2025-10-02T14:34:11.618Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/00/60f9ea3bb697667a14314d7269956f58bf56bb73864f8f8d52a3c2535e9a/xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d", size = 27863, upload-time = "2025-10-02T14:34:12.619Z" },
+ { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" },
+ { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" },
+ { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" },
+ { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" },
+ { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" },
+ { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" },
+ { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" },
+ { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" },
+ { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" },
+ { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" },
+ { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" },
+ { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" },
+ { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" },
+ { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" },
+ { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" },
+ { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" },
+ { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" },
+ { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" },
+ { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" },
+ { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" },
+ { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" },
+ { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" },
+ { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" },
+ { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" },
+ { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" },
+ { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" },
+ { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" },
+ { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" },
+ { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" },
+ { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" },
+ { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" },
+ { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" },
+ { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" },
+ { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" },
+ { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" },
+ { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" },
+ { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" },
+ { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" },
+ { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" },
+]
+
+[[package]]
+name = "zipp"
+version = "3.23.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
+]
+
+[[package]]
+name = "zstandard"
+version = "0.25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" },
+ { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" },
+ { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" },
+ { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" },
+ { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" },
+ { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" },
+ { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" },
+ { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" },
+ { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" },
+ { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" },
+ { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" },
+ { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" },
+ { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" },
+ { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" },
+ { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" },
+ { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" },
+ { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" },
+ { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" },
+ { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" },
+ { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" },
+ { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" },
+ { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" },
+ { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" },
+ { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" },
+ { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" },
+ { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" },
+]
diff --git a/langgraph/source/libs/langgraph/LICENSE b/langgraph/source/libs/langgraph/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..fc0602feecdd6748623c852ab534e1ca612673c7
--- /dev/null
+++ b/langgraph/source/libs/langgraph/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 LangChain, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/langgraph/source/libs/langgraph/Makefile b/langgraph/source/libs/langgraph/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..86852414a60fd1c4e122ca67b08adc6addb1ab15
--- /dev/null
+++ b/langgraph/source/libs/langgraph/Makefile
@@ -0,0 +1,156 @@
+.PHONY: all format lint test test_watch integration_tests spell_check spell_fix benchmark profile start-dev-server integration_tests
+
+# Default target executed when no arguments are given to make.
+all: help
+
+######################
+# TESTING AND COVERAGE
+######################
+
+# Benchmarks
+
+OUTPUT ?= out/benchmark.json
+
+install: ## Install dependencies
+ uv sync --frozen --all-extras --all-packages --group dev
+
+benchmark:
+ mkdir -p out
+ rm -f $(OUTPUT)
+ uv run python -m bench -o $(OUTPUT) --rigorous
+
+benchmark-fast:
+ mkdir -p out
+ rm -f $(OUTPUT)
+ uv run python -m bench -o $(OUTPUT) --fast
+
+GRAPH ?= bench/fanout_to_subgraph.py
+
+profile:
+ mkdir -p out
+ sudo uv run py-spy record -g -o out/profile.svg -- python $(GRAPH)
+
+# Run unit tests and generate a coverage report.
+coverage:
+ uv run pytest --cov \
+ --cov-config=.coveragerc \
+ --cov-report xml \
+ --cov-report term-missing:skip-covered
+
+start-services:
+ docker compose -f tests/compose-postgres.yml -f tests/compose-redis.yml up -V --force-recreate --wait --remove-orphans
+
+stop-services:
+ docker compose -f tests/compose-postgres.yml -f tests/compose-redis.yml down -v
+
+start-dev-server:
+ LOG_LEVEL=warning uv run langgraph dev --config tests/example_app/langgraph.json --no-browser & echo "$$!" > .devserver.pid
+ @echo "Dev server started."
+
+stop-dev-server:
+ @if [ -f .devserver.pid ]; then \
+ kill `cat .devserver.pid` && rm .devserver.pid; \
+ echo "Dev server stopped."; \
+ else \
+ echo "No dev server PID file found."; \
+ fi
+
+TEST ?= .
+NO_DOCKER ?= $(sh command -v docker >/dev/null 2>&1 && echo "false" || echo "true")
+
+test:
+ if [ "$(NO_DOCKER)" = "false" ]; then \
+ make start-services &&\
+ make start-dev-server &&\
+ uv run pytest $(TEST); \
+ EXIT_CODE=$$?; \
+ make stop-services; \
+ make stop-dev-server; \
+ exit $$EXIT_CODE; \
+ else \
+ NO_DOCKER=true uv run pytest $(TEST) ; \
+ EXIT_CODE=$$?; \
+ exit $$EXIT_CODE; \
+ fi
+
+test_parallel:
+ make start-services &&\
+ make start-dev-server &&\
+ uv run pytest -n auto --dist worksteal $(TEST) -vv --lf; \
+ EXIT_CODE=$$?; \
+ make stop-services; \
+ make stop-dev-server; \
+ exit $$EXIT_CODE
+
+integration_tests:
+ uv run pytest integration_tests
+
+WORKERS ?= auto
+XDIST_ARGS := $(if $(WORKERS),-n $(WORKERS) --dist worksteal,)
+MAXFAIL ?=
+MAXFAIL_ARGS := $(if $(MAXFAIL),--maxfail $(MAXFAIL),)
+# Add an '-x' if xdist is enabled
+XDIST_ARGS := $(if $(WORKERS),-x $(XDIST_ARGS),)
+
+test_watch:
+ make start-services &&\
+ make start-dev-server &&\
+ uv run ptw . -- --ff -vv $(XDIST_ARGS) $(MAXFAIL_ARGS) $(TEST); \
+ EXIT_CODE=$$?; \
+ make stop-services; \
+ make stop-dev-server; \
+ exit $$EXIT_CODE
+
+test_watch_all:
+ npx concurrently -n langgraph,checkpoint,checkpoint-sqlite,postgres "make test_watch" "make -C ../checkpoint test_watch" "make -C ../checkpoint-sqlite test_watch" "make -C ../checkpoint-postgres test_watch"
+
+
+######################
+# LINTING AND FORMATTING
+######################
+
+# Define a variable for Python and notebook files.
+PYTHON_FILES=.
+MYPY_CACHE=.mypy_cache
+lint format: PYTHON_FILES=.
+lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --relative --diff-filter=d main . | grep -E r'\.py$$|\.ipynb$$')
+lint_package: PYTHON_FILES=langgraph
+lint_tests: PYTHON_FILES=tests
+lint_tests: MYPY_CACHE=.mypy_cache_test
+
+lint lint_diff lint_package lint_tests:
+ uv run ruff check .
+ [ "$(PYTHON_FILES)" = "" ] || uv run ruff format $(PYTHON_FILES) --diff
+ [ "$(PYTHON_FILES)" = "" ] || uv run ruff check --select I $(PYTHON_FILES)
+ [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE)
+ [ "$(PYTHON_FILES)" = "" ] || uv run mypy langgraph --cache-dir $(MYPY_CACHE)
+
+format format_diff:
+ uv run ruff format $(PYTHON_FILES)
+ uv run ruff check --select I --fix $(PYTHON_FILES)
+
+spell_check:
+ uv run codespell --toml pyproject.toml
+
+spell_fix:
+ uv run codespell --toml pyproject.toml -w
+
+
+######################
+# HELP
+######################
+
+help:
+ @echo '===================='
+ @echo '-- DOCUMENTATION --'
+
+ @echo '-- LINTING --'
+ @echo 'format - run code formatters'
+ @echo 'lint - run linters'
+ @echo 'spell_check - run codespell on the project'
+ @echo 'spell_fix - run codespell on the project and fix the errors'
+ @echo '-- TESTS --'
+ @echo 'coverage - run unit tests and generate coverage report'
+ @echo 'test - run unit tests'
+ @echo 'test TEST_FILE= - run all tests in file'
+ @echo 'test_watch - run unit tests in watch mode'
diff --git a/langgraph/source/libs/langgraph/README.md b/langgraph/source/libs/langgraph/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f7eb802472bfe29c7945b18491c9c6274ff7efb8
--- /dev/null
+++ b/langgraph/source/libs/langgraph/README.md
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+[](https://pypi.org/project/langgraph/)
+[](https://pepy.tech/project/langgraph)
+[](https://github.com/langchain-ai/langgraph/issues)
+[](https://docs.langchain.com/oss/python/langgraph/overview)
+
+Trusted by companies shaping the future of agents – including Klarna, Replit, Elastic, and more – LangGraph is a low-level orchestration framework for building, managing, and deploying long-running, stateful agents.
+
+## Get started
+
+Install LangGraph:
+
+```
+pip install -U langgraph
+```
+
+Create a simple workflow:
+
+```python
+from langgraph.graph import START, StateGraph
+from typing_extensions import TypedDict
+
+
+class State(TypedDict):
+ text: str
+
+
+def node_a(state: State) -> dict:
+ return {"text": state["text"] + "a"}
+
+
+def node_b(state: State) -> dict:
+ return {"text": state["text"] + "b"}
+
+
+graph = StateGraph(State)
+graph.add_node("node_a", node_a)
+graph.add_node("node_b", node_b)
+graph.add_edge(START, "node_a")
+graph.add_edge("node_a", "node_b")
+
+print(graph.compile().invoke({"text": ""}))
+# {'text': 'ab'}
+```
+
+Get started with the [LangGraph Quickstart](https://docs.langchain.com/oss/python/langgraph/quickstart).
+
+To quickly build agents with LangChain's `create_agent` (built on LangGraph), see the [LangChain Agents documentation](https://docs.langchain.com/oss/python/langchain/agents).
+
+## Core benefits
+
+LangGraph provides low-level supporting infrastructure for *any* long-running, stateful workflow or agent. LangGraph does not abstract prompts or architecture, and provides the following central benefits:
+
+- [Durable execution](https://docs.langchain.com/oss/python/langgraph/durable-execution): Build agents that persist through failures and can run for extended periods, automatically resuming from exactly where they left off.
+- [Human-in-the-loop](https://docs.langchain.com/oss/python/langgraph/interrupts): Seamlessly incorporate human oversight by inspecting and modifying agent state at any point during execution.
+- [Comprehensive memory](https://docs.langchain.com/oss/python/langgraph/memory): Create truly stateful agents with both short-term working memory for ongoing reasoning and long-term persistent memory across sessions.
+- [Debugging with LangSmith](http://www.langchain.com/langsmith): Gain deep visibility into complex agent behavior with visualization tools that trace execution paths, capture state transitions, and provide detailed runtime metrics.
+- [Production-ready deployment](https://docs.langchain.com/langsmith/app-development): Deploy sophisticated agent systems confidently with scalable infrastructure designed to handle the unique challenges of stateful, long-running workflows.
+
+## LangGraph’s ecosystem
+
+While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
+
+- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
+- [LangSmith Deployment](https://docs.langchain.com/langsmith/deployments) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://docs.langchain.com/oss/python/langgraph/studio).
+- [LangChain](https://docs.langchain.com/oss/python/langchain/overview) – Provides integrations and composable components to streamline LLM application development.
+
+> [!NOTE]
+> Looking for the JS version of LangGraph? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://docs.langchain.com/oss/javascript/langgraph/overview).
+
+## Additional resources
+
+- [Guides](https://docs.langchain.com/oss/python/langgraph/guides): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
+- [Reference](https://reference.langchain.com/python/langgraph/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
+- [Examples](https://docs.langchain.com/oss/python/langgraph/agentic-rag): Guided examples on getting started with LangGraph.
+- [LangChain Forum](https://forum.langchain.com/): Connect with the community and share all of your technical questions, ideas, and feedback.
+- [LangChain Academy](https://academy.langchain.com/courses/intro-to-langgraph): Learn the basics of LangGraph in our free, structured course.
+- [Case studies](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship AI applications at scale.
+
+## Acknowledgements
+
+LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
diff --git a/langgraph/source/libs/langgraph/__init__.py b/langgraph/source/libs/langgraph/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/langgraph/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/langgraph/bench/__init__.py b/langgraph/source/libs/langgraph/bench/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/langgraph/bench/__main__.py b/langgraph/source/libs/langgraph/bench/__main__.py
new file mode 100644
index 0000000000000000000000000000000000000000..0bfa16ad12eb4b03a5abd2eb813033acfb156e04
--- /dev/null
+++ b/langgraph/source/libs/langgraph/bench/__main__.py
@@ -0,0 +1,515 @@
+import random
+from uuid import uuid4
+
+from langchain_core.messages import HumanMessage
+from langgraph.checkpoint.memory import InMemorySaver
+from pyperf._runner import Runner
+from uvloop import new_event_loop
+
+from bench.fanout_to_subgraph import fanout_to_subgraph, fanout_to_subgraph_sync
+from bench.pydantic_state import pydantic_state
+from bench.react_agent import react_agent
+from bench.sequential import create_sequential
+from bench.wide_dict import wide_dict
+from bench.wide_state import wide_state
+from langgraph.graph import StateGraph
+from langgraph.pregel import Pregel
+
+
+async def arun(graph: Pregel, input: dict):
+ len(
+ [
+ c
+ async for c in graph.astream(
+ input,
+ {
+ "configurable": {"thread_id": str(uuid4())},
+ "recursion_limit": 1000000000,
+ },
+ durability="exit",
+ )
+ ]
+ )
+
+
+async def arun_first_event_latency(graph: Pregel, input: dict) -> None:
+ """Latency for the first event.
+
+ Run the graph until the first event is processed and then stop.
+ """
+ stream = graph.astream(
+ input,
+ {
+ "configurable": {"thread_id": str(uuid4())},
+ "recursion_limit": 1000000000,
+ },
+ durability="exit",
+ )
+
+ try:
+ async for _ in stream:
+ break
+ finally:
+ await stream.aclose()
+
+
+def run(graph: Pregel, input: dict):
+ len(
+ [
+ c
+ for c in graph.stream(
+ input,
+ {
+ "configurable": {"thread_id": str(uuid4())},
+ "recursion_limit": 1000000000,
+ },
+ durability="exit",
+ )
+ ]
+ )
+
+
+def run_first_event_latency(graph: Pregel, input: dict) -> None:
+ """Latency for the first event.
+
+ Run the graph until the first event is processed and then stop.
+ """
+ stream = graph.stream(
+ input,
+ {
+ "configurable": {"thread_id": str(uuid4())},
+ "recursion_limit": 1000000000,
+ },
+ durability="exit",
+ )
+
+ try:
+ for _ in stream:
+ break
+ finally:
+ stream.close()
+
+
+def compile_graph(graph: StateGraph) -> None:
+ """Compile the graph."""
+ graph.compile()
+
+
+benchmarks = (
+ (
+ "fanout_to_subgraph_10x",
+ fanout_to_subgraph().compile(checkpointer=None),
+ fanout_to_subgraph_sync().compile(checkpointer=None),
+ {
+ "subjects": [
+ random.choices("abcdefghijklmnopqrstuvwxyz", k=1000) for _ in range(10)
+ ]
+ },
+ ),
+ (
+ "fanout_to_subgraph_10x_checkpoint",
+ fanout_to_subgraph().compile(checkpointer=InMemorySaver()),
+ fanout_to_subgraph_sync().compile(checkpointer=InMemorySaver()),
+ {
+ "subjects": [
+ random.choices("abcdefghijklmnopqrstuvwxyz", k=1000) for _ in range(10)
+ ]
+ },
+ ),
+ (
+ "fanout_to_subgraph_100x",
+ fanout_to_subgraph().compile(checkpointer=None),
+ fanout_to_subgraph_sync().compile(checkpointer=None),
+ {
+ "subjects": [
+ random.choices("abcdefghijklmnopqrstuvwxyz", k=1000) for _ in range(100)
+ ]
+ },
+ ),
+ (
+ "fanout_to_subgraph_100x_checkpoint",
+ fanout_to_subgraph().compile(checkpointer=InMemorySaver()),
+ fanout_to_subgraph_sync().compile(checkpointer=InMemorySaver()),
+ {
+ "subjects": [
+ random.choices("abcdefghijklmnopqrstuvwxyz", k=1000) for _ in range(100)
+ ]
+ },
+ ),
+ (
+ "react_agent_10x",
+ react_agent(10, checkpointer=None),
+ react_agent(10, checkpointer=None),
+ {"messages": [HumanMessage("hi?")]},
+ ),
+ (
+ "react_agent_10x_checkpoint",
+ react_agent(10, checkpointer=InMemorySaver()),
+ react_agent(10, checkpointer=InMemorySaver()),
+ {"messages": [HumanMessage("hi?")]},
+ ),
+ (
+ "react_agent_100x",
+ react_agent(100, checkpointer=None),
+ react_agent(100, checkpointer=None),
+ {"messages": [HumanMessage("hi?")]},
+ ),
+ (
+ "react_agent_100x_checkpoint",
+ react_agent(100, checkpointer=InMemorySaver()),
+ react_agent(100, checkpointer=InMemorySaver()),
+ {"messages": [HumanMessage("hi?")]},
+ ),
+ (
+ "wide_state_25x300",
+ wide_state(300).compile(checkpointer=None),
+ wide_state(300).compile(checkpointer=None),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(5)
+ }
+ for i in range(5)
+ }
+ ]
+ },
+ ),
+ (
+ "wide_state_25x300_checkpoint",
+ wide_state(300).compile(checkpointer=InMemorySaver()),
+ wide_state(300).compile(checkpointer=InMemorySaver()),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(5)
+ }
+ for i in range(5)
+ }
+ ]
+ },
+ ),
+ (
+ "wide_state_15x600",
+ wide_state(600).compile(checkpointer=None),
+ wide_state(600).compile(checkpointer=None),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(5)
+ }
+ for i in range(3)
+ }
+ ]
+ },
+ ),
+ (
+ "wide_state_15x600_checkpoint",
+ wide_state(600).compile(checkpointer=InMemorySaver()),
+ wide_state(600).compile(checkpointer=InMemorySaver()),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(5)
+ }
+ for i in range(3)
+ }
+ ]
+ },
+ ),
+ (
+ "wide_state_9x1200",
+ wide_state(1200).compile(checkpointer=None),
+ wide_state(1200).compile(checkpointer=None),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(3)
+ }
+ for i in range(3)
+ }
+ ]
+ },
+ ),
+ (
+ "wide_state_9x1200_checkpoint",
+ wide_state(1200).compile(checkpointer=InMemorySaver()),
+ wide_state(1200).compile(checkpointer=InMemorySaver()),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(3)
+ }
+ for i in range(3)
+ }
+ ]
+ },
+ ),
+ (
+ "wide_dict_25x300",
+ wide_dict(300).compile(checkpointer=None),
+ wide_dict(300).compile(checkpointer=None),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(5)
+ }
+ for i in range(5)
+ }
+ ]
+ },
+ ),
+ (
+ "wide_dict_25x300_checkpoint",
+ wide_dict(300).compile(checkpointer=InMemorySaver()),
+ wide_dict(300).compile(checkpointer=InMemorySaver()),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(5)
+ }
+ for i in range(5)
+ }
+ ]
+ },
+ ),
+ (
+ "wide_dict_15x600",
+ wide_dict(600).compile(checkpointer=None),
+ wide_dict(600).compile(checkpointer=None),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(5)
+ }
+ for i in range(3)
+ }
+ ]
+ },
+ ),
+ (
+ "wide_dict_15x600_checkpoint",
+ wide_dict(600).compile(checkpointer=InMemorySaver()),
+ wide_dict(600).compile(checkpointer=InMemorySaver()),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(5)
+ }
+ for i in range(3)
+ }
+ ]
+ },
+ ),
+ (
+ "wide_dict_9x1200",
+ wide_dict(1200).compile(checkpointer=None),
+ wide_dict(1200).compile(checkpointer=None),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(3)
+ }
+ for i in range(3)
+ }
+ ]
+ },
+ ),
+ (
+ "wide_dict_9x1200_checkpoint",
+ wide_dict(1200).compile(checkpointer=InMemorySaver()),
+ wide_dict(1200).compile(checkpointer=InMemorySaver()),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(3)
+ }
+ for i in range(3)
+ }
+ ]
+ },
+ ),
+ (
+ "sequential_10",
+ create_sequential(10).compile(),
+ create_sequential(10).compile(),
+ {"messages": []}, # Empty list of messages
+ ),
+ (
+ "sequential_1000",
+ create_sequential(1000).compile(),
+ create_sequential(1000).compile(),
+ {"messages": []}, # Empty list of messages
+ ),
+ (
+ "pydantic_state_25x300",
+ pydantic_state(300).compile(checkpointer=None),
+ pydantic_state(300).compile(checkpointer=None),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(5)
+ }
+ for i in range(5)
+ }
+ ]
+ },
+ ),
+ (
+ "pydantic_state_25x300_checkpoint",
+ pydantic_state(300).compile(checkpointer=InMemorySaver()),
+ pydantic_state(300).compile(checkpointer=InMemorySaver()),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(5)
+ }
+ for i in range(5)
+ }
+ ]
+ },
+ ),
+ (
+ "pydantic_state_15x600",
+ pydantic_state(600).compile(checkpointer=None),
+ pydantic_state(600).compile(checkpointer=None),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(5)
+ }
+ for i in range(3)
+ }
+ ]
+ },
+ ),
+ (
+ "pydantic_state_15x600_checkpoint",
+ pydantic_state(600).compile(checkpointer=InMemorySaver()),
+ pydantic_state(600).compile(checkpointer=InMemorySaver()),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(5)
+ }
+ for i in range(3)
+ }
+ ]
+ },
+ ),
+ (
+ "pydantic_state_9x1200",
+ pydantic_state(1200).compile(checkpointer=None),
+ pydantic_state(1200).compile(checkpointer=None),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(3)
+ }
+ for i in range(3)
+ }
+ ]
+ },
+ ),
+ (
+ "pydantic_state_9x1200_checkpoint",
+ pydantic_state(1200).compile(checkpointer=InMemorySaver()),
+ pydantic_state(1200).compile(checkpointer=InMemorySaver()),
+ {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(3)
+ }
+ for i in range(3)
+ }
+ ]
+ },
+ ),
+)
+
+
+r = Runner()
+
+# Full graph run time
+for name, agraph, graph, input in benchmarks:
+ r.bench_async_func(name, arun, agraph, input, loop_factory=new_event_loop)
+ if graph is not None:
+ r.bench_func(name + "_sync", run, graph, input)
+
+
+# Pick a handful of graphs to measure the first event latency.
+# At the moment, limiting just due to the size of the annotation on github.
+GRAPHS_FOR_1st_EVENT_LATENCY = (
+ "sequential_1000",
+ "pydantic_state_25x300",
+)
+
+# First event latency
+for name, agraph, graph, input in benchmarks:
+ if graph not in GRAPHS_FOR_1st_EVENT_LATENCY:
+ continue
+ r.bench_async_func(
+ name + "_first_event_latency",
+ arun_first_event_latency,
+ agraph,
+ input,
+ loop_factory=new_event_loop,
+ )
+ if graph is not None:
+ r.bench_func(
+ name + "_first_event_latency_sync", run_first_event_latency, graph, input
+ )
+
+# Graph compilation times
+compilation_benchmarks = (
+ (
+ "sequential_1000",
+ create_sequential(1_000),
+ ),
+ (
+ "pydantic_state_25x300",
+ pydantic_state(300),
+ ),
+ (
+ "wide_state_15x600",
+ wide_state(600),
+ ),
+)
+
+for name, graph in compilation_benchmarks:
+ r.bench_func(name + "_compilation", compile_graph, graph)
diff --git a/langgraph/source/libs/langgraph/bench/fanout_to_subgraph.py b/langgraph/source/libs/langgraph/bench/fanout_to_subgraph.py
new file mode 100644
index 0000000000000000000000000000000000000000..491d0d301c3c06f81e743da13c13f6dc119c3fc7
--- /dev/null
+++ b/langgraph/source/libs/langgraph/bench/fanout_to_subgraph.py
@@ -0,0 +1,134 @@
+import operator
+from typing import Annotated
+
+from typing_extensions import TypedDict
+
+from langgraph.constants import END, START
+from langgraph.graph.state import StateGraph
+from langgraph.types import Send
+
+
+def fanout_to_subgraph() -> StateGraph:
+ class OverallState(TypedDict):
+ subjects: list[str]
+ jokes: Annotated[list[str], operator.add]
+
+ async def continue_to_jokes(state: OverallState):
+ return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]
+
+ class JokeInput(TypedDict):
+ subject: str
+
+ class JokeOutput(TypedDict):
+ jokes: list[str]
+
+ class JokeState(JokeInput, JokeOutput): ...
+
+ async def bump(state: JokeOutput):
+ return {"jokes": [state["jokes"][0] + " a"]}
+
+ async def generate(state: JokeInput):
+ return {"jokes": [f"Joke about {state['subject']}"]}
+
+ async def edit(state: JokeInput):
+ subject = state["subject"]
+ return {"subject": f"{subject} - hohoho"}
+
+ async def bump_loop(state: JokeOutput):
+ return END if state["jokes"][0].endswith(" a" * 10) else "bump"
+
+ # subgraph
+ subgraph = StateGraph(JokeState, input_schema=JokeInput, output_schema=JokeOutput)
+ subgraph.add_node("edit", edit)
+ subgraph.add_node("generate", generate)
+ subgraph.add_node("bump", bump)
+ subgraph.set_entry_point("edit")
+ subgraph.add_edge("edit", "generate")
+ subgraph.add_edge("generate", "bump")
+ subgraph.add_conditional_edges("bump", bump_loop)
+ subgraph.set_finish_point("generate")
+ subgraphc = subgraph.compile()
+
+ # parent graph
+ builder = StateGraph(OverallState)
+ builder.add_node("generate_joke", subgraphc)
+ builder.add_conditional_edges(START, continue_to_jokes)
+ builder.add_edge("generate_joke", END)
+
+ return builder
+
+
+def fanout_to_subgraph_sync() -> StateGraph:
+ class OverallState(TypedDict):
+ subjects: list[str]
+ jokes: Annotated[list[str], operator.add]
+
+ def continue_to_jokes(state: OverallState):
+ return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]
+
+ class JokeInput(TypedDict):
+ subject: str
+
+ class JokeOutput(TypedDict):
+ jokes: list[str]
+
+ class JokeState(JokeInput, JokeOutput): ...
+
+ def bump(state: JokeOutput):
+ return {"jokes": [state["jokes"][0] + " a"]}
+
+ def generate(state: JokeInput):
+ return {"jokes": [f"Joke about {state['subject']}"]}
+
+ def edit(state: JokeInput):
+ subject = state["subject"]
+ return {"subject": f"{subject} - hohoho"}
+
+ def bump_loop(state: JokeOutput):
+ return END if state["jokes"][0].endswith(" a" * 10) else "bump"
+
+ # subgraph
+ subgraph = StateGraph(JokeState, input_schema=JokeInput, output_schema=JokeOutput)
+ subgraph.add_node("edit", edit)
+ subgraph.add_node("generate", generate)
+ subgraph.add_node("bump", bump)
+ subgraph.set_entry_point("edit")
+ subgraph.add_edge("edit", "generate")
+ subgraph.add_edge("generate", "bump")
+ subgraph.add_conditional_edges("bump", bump_loop)
+ subgraph.set_finish_point("generate")
+ subgraphc = subgraph.compile()
+
+ # parent graph
+ builder = StateGraph(OverallState)
+ builder.add_node("generate_joke", subgraphc)
+ builder.add_conditional_edges(START, continue_to_jokes)
+ builder.add_edge("generate_joke", END)
+
+ return builder
+
+
+if __name__ == "__main__":
+ import asyncio
+ import random
+ import time
+
+ import uvloop
+ from langgraph.checkpoint.memory import InMemorySaver
+
+ graph = fanout_to_subgraph().compile(checkpointer=InMemorySaver())
+ input = {
+ "subjects": [
+ random.choices("abcdefghijklmnopqrstuvwxyz", k=1000) for _ in range(1000)
+ ]
+ }
+ config = {"configurable": {"thread_id": "1"}}
+
+ async def run():
+ len([c async for c in graph.astream(input, config=config)])
+
+ uvloop.install()
+ start = time.time()
+ asyncio.run(run())
+ end = time.time()
+ print(f"Time taken: {end - start:.4f} seconds")
diff --git a/langgraph/source/libs/langgraph/bench/pydantic_state.py b/langgraph/source/libs/langgraph/bench/pydantic_state.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f737bf7a999e9cb13c8c927be5acdaceed0635c
--- /dev/null
+++ b/langgraph/source/libs/langgraph/bench/pydantic_state.py
@@ -0,0 +1,327 @@
+import operator
+from collections.abc import Sequence
+from functools import partial
+from random import choice
+from typing import Annotated
+
+from pydantic import BaseModel, Field, field_validator
+
+from langgraph.constants import END, START
+from langgraph.graph.state import StateGraph
+
+
+def pydantic_state(n: int) -> StateGraph:
+ class State(BaseModel):
+ messages: Annotated[list, operator.add] = Field(default_factory=list)
+
+ @field_validator("messages", mode="after")
+ @classmethod
+ def validate_messages(cls, v):
+ if not isinstance(v, list):
+ raise TypeError("messages must be a list")
+ for msg in v:
+ if not isinstance(msg, dict):
+ raise TypeError("messages must be a list of dicts")
+ if not all(isinstance(k, str) for k in msg.keys()):
+ raise TypeError("messages must be a list of dicts with str keys")
+ return v
+
+ trigger_events: Annotated[list, operator.add] = Field(default_factory=list)
+ """The external events that are converted by the graph."""
+
+ @field_validator("trigger_events", mode="after")
+ @classmethod
+ def validate_trigger_events(cls, v):
+ if not isinstance(v, list):
+ raise TypeError("trigger_events must be a list")
+ for event in v:
+ if not isinstance(event, dict):
+ raise TypeError("trigger_events must be a list of dicts")
+ if not all(isinstance(k, str) for k in event.keys()):
+ raise TypeError(
+ "trigger_events must be a list of dicts with str keys"
+ )
+ return v
+
+ primary_issue_medium: Annotated[str, lambda x, y: y or x] = Field(
+ default="email"
+ )
+ """The primary issue medium for the current conversation."""
+
+ @field_validator("primary_issue_medium", mode="after")
+ @classmethod
+ def validate_primary_issue_medium(cls, v):
+ if not isinstance(v, str):
+ raise TypeError("primary_issue_medium must be a string")
+ return v
+
+ autoresponse: Annotated[dict | None, lambda _, y: y] = Field(
+ default=None
+ ) # Always overwrite
+
+ @field_validator("autoresponse", mode="after")
+ @classmethod
+ def validate_autoresponse(cls, v):
+ if v is not None and not isinstance(v, dict):
+ raise TypeError("autoresponse must be a dict or None")
+ return v
+
+ issue: Annotated[dict | None, lambda x, y: y if y else x] = Field(default=None)
+
+ @field_validator("issue", mode="after")
+ @classmethod
+ def validate_issue(cls, v):
+ if v is not None and not isinstance(v, dict):
+ raise TypeError("issue must be a dict or None")
+ return v
+
+ relevant_rules: list[dict] | None = Field(default=None)
+ """SOPs fetched from the rulebook that are relevant to the current conversation."""
+
+ @field_validator("relevant_rules", mode="after")
+ @classmethod
+ def validate_relevant_rules(cls, v):
+ if v is None:
+ return v
+ if not isinstance(v, list):
+ raise TypeError("relevant_rules must be a list or None")
+ for rule in v:
+ if not isinstance(rule, dict):
+ raise TypeError("relevant_rules must be a list of dicts")
+ if not all(isinstance(k, str) for k in rule.keys()):
+ raise TypeError(
+ "relevant_rules must be a list of dicts with str keys"
+ )
+ return v
+
+ memory_docs: list[dict] | None = Field(default=None)
+ """Memory docs fetched from the memory service that are relevant to the current conversation."""
+
+ @field_validator("memory_docs", mode="after")
+ @classmethod
+ def validate_memory_docs(cls, v):
+ if v is None:
+ return v
+ if not isinstance(v, list):
+ raise TypeError("memory_docs must be a list or None")
+ for doc in v:
+ if not isinstance(doc, dict):
+ raise TypeError("memory_docs must be a list of dicts")
+ if not all(isinstance(k, str) for k in doc.keys()):
+ raise TypeError("memory_docs must be a list of dicts with str keys")
+ return v
+
+ categorizations: Annotated[list[dict], operator.add] = Field(
+ default_factory=list
+ )
+ """The issue categorizations auto-generated by the AI."""
+
+ @field_validator("categorizations", mode="after")
+ @classmethod
+ def validate_categorizations(cls, v):
+ if not isinstance(v, list):
+ raise TypeError("categorizations must be a list")
+ for categorization in v:
+ if not isinstance(categorization, dict):
+ raise TypeError("categorizations must be a list of dicts")
+ if not all(isinstance(k, str) for k in categorization.keys()):
+ raise TypeError(
+ "categorizations must be a list of dicts with str keys"
+ )
+ return v
+
+ responses: Annotated[list[dict], operator.add] = Field(default_factory=list)
+ """The draft responses recommended by the AI."""
+
+ @field_validator("responses", mode="after")
+ @classmethod
+ def validate_responses(cls, v):
+ if not isinstance(v, list):
+ raise TypeError("responses must be a list")
+ for response in v:
+ if not isinstance(response, dict):
+ raise TypeError("responses must be a list of dicts")
+ if not all(isinstance(k, str) for k in response.keys()):
+ raise TypeError("responses must be a list of dicts with str keys")
+ return v
+
+ user_info: Annotated[dict | None, lambda x, y: y if y is not None else x] = (
+ Field(default=None)
+ )
+ """The current user state (by email)."""
+
+ @field_validator("user_info", mode="after")
+ @classmethod
+ def validate_user_info(cls, v):
+ if v is not None and not isinstance(v, dict):
+ raise TypeError("user_info must be a dict or None")
+ return v
+
+ crm_info: Annotated[dict | None, lambda x, y: y if y is not None else x] = (
+ Field(default=None)
+ )
+ """The CRM information for organization the current user is from."""
+
+ @field_validator("crm_info", mode="after")
+ @classmethod
+ def validate_crm_info(cls, v):
+ if v is not None and not isinstance(v, dict):
+ raise TypeError("crm_info must be a dict or None")
+ return v
+
+ email_thread_id: Annotated[
+ str | None, lambda x, y: y if y is not None else x
+ ] = Field(default=None)
+ """The current email thread ID."""
+
+ @field_validator("email_thread_id", mode="after")
+ @classmethod
+ def validate_email_thread_id(cls, v):
+ if v is not None and not isinstance(v, str):
+ raise TypeError("email_thread_id must be a string or None")
+ return v
+
+ slack_participants: Annotated[dict, operator.or_] = Field(default_factory=dict)
+ """The growing list of current slack participants."""
+
+ @field_validator("slack_participants", mode="after")
+ @classmethod
+ def validate_slack_participants(cls, v):
+ if not isinstance(v, dict):
+ raise TypeError("slack_participants must be a dict")
+ for participant in v:
+ if not isinstance(participant, str):
+ raise TypeError("slack_participants must be a dict with str keys")
+ return v
+
+ bot_id: str | None = Field(default=None)
+ """The ID of the bot user in the slack channel."""
+
+ @field_validator("bot_id", mode="after")
+ @classmethod
+ def validate_bot_id(cls, v):
+ if v is not None and not isinstance(v, str):
+ raise TypeError("bot_id must be a string or None")
+ return v
+
+ notified_assignees: Annotated[dict, operator.or_] = Field(default_factory=dict)
+
+ @field_validator("notified_assignees", mode="after")
+ def validate_notified_assignees(cls, v):
+ if not isinstance(v, dict):
+ raise TypeError("notified_assignees must be a dict")
+ for assignee in v:
+ if not isinstance(assignee, str):
+ raise TypeError("notified_assignees must be a dict with str keys")
+ return v
+
+ list_fields = {
+ "messages",
+ "trigger_events",
+ "categorizations",
+ "responses",
+ "memory_docs",
+ "relevant_rules",
+ }
+ dict_fields = {
+ "user_info",
+ "crm_info",
+ "slack_participants",
+ "notified_assignees",
+ "autoresponse",
+ "issue",
+ }
+
+ def read_write(read: str, write: Sequence[str], input: State) -> dict:
+ val = getattr(input, read)
+ val = {val: val} if isinstance(val, str) else val
+ val_single = val[-1] if isinstance(val, list) else val
+ val_list = val if isinstance(val, list) else [val]
+ return {
+ k: val_list
+ if k in list_fields
+ else val_single
+ if k in dict_fields
+ else "".join(choice("abcdefghijklmnopqrstuvwxyz") for _ in range(n))
+ for k in write
+ }
+
+ builder = StateGraph(State)
+ builder.add_edge(START, "one")
+ builder.add_node(
+ "one",
+ partial(read_write, "messages", ["trigger_events", "primary_issue_medium"]),
+ )
+ builder.add_edge("one", "two")
+ builder.add_node(
+ "two",
+ partial(read_write, "trigger_events", ["autoresponse", "issue"]),
+ )
+ builder.add_edge("two", "three")
+ builder.add_edge("two", "four")
+ builder.add_node(
+ "three",
+ partial(read_write, "autoresponse", ["relevant_rules"]),
+ )
+ builder.add_node(
+ "four",
+ partial(
+ read_write,
+ "trigger_events",
+ ["categorizations", "responses", "memory_docs"],
+ ),
+ )
+ builder.add_node(
+ "five",
+ partial(
+ read_write,
+ "categorizations",
+ [
+ "user_info",
+ "crm_info",
+ "email_thread_id",
+ "slack_participants",
+ "bot_id",
+ "notified_assignees",
+ ],
+ ),
+ )
+ builder.add_edge(["three", "four"], "five")
+ builder.add_edge("five", "six")
+ builder.add_node(
+ "six",
+ partial(read_write, "responses", ["messages"]),
+ )
+ builder.add_conditional_edges(
+ "six", lambda state: END if len(state.messages) > n else "one"
+ )
+
+ return builder
+
+
+if __name__ == "__main__":
+ import asyncio
+
+ import uvloop
+ from langgraph.checkpoint.memory import InMemorySaver
+
+ graph = pydantic_state(1000).compile(checkpointer=InMemorySaver())
+ input = {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(5)
+ }
+ for i in range(5)
+ }
+ ]
+ }
+ config = {"configurable": {"thread_id": "1"}, "recursion_limit": 20000000000}
+
+ async def run():
+ async for c in graph.astream(input, config=config):
+ print(c.keys())
+
+ uvloop.install()
+ asyncio.run(run())
diff --git a/langgraph/source/libs/langgraph/bench/react_agent.py b/langgraph/source/libs/langgraph/bench/react_agent.py
new file mode 100644
index 0000000000000000000000000000000000000000..9834e3b913041a744613b21b2adb0be47656ef8f
--- /dev/null
+++ b/langgraph/source/libs/langgraph/bench/react_agent.py
@@ -0,0 +1,80 @@
+from typing import Any
+from uuid import uuid4
+
+from langchain_core.callbacks import CallbackManagerForLLMRun
+from langchain_core.language_models.fake_chat_models import (
+ FakeMessagesListChatModel,
+)
+from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
+from langchain_core.outputs import ChatGeneration, ChatResult
+from langchain_core.tools import StructuredTool
+from langgraph.checkpoint.base import BaseCheckpointSaver
+from langgraph.prebuilt.chat_agent_executor import create_react_agent
+
+from langgraph.pregel import Pregel
+
+
+def react_agent(n_tools: int, checkpointer: BaseCheckpointSaver | None) -> Pregel:
+ class FakeFunctionChatModel(FakeMessagesListChatModel):
+ def bind_tools(self, functions: list):
+ return self
+
+ def _generate(
+ self,
+ messages: list[BaseMessage],
+ stop: list[str] | None = None,
+ run_manager: CallbackManagerForLLMRun | None = None,
+ **kwargs: Any,
+ ) -> ChatResult:
+ response = self.responses[self.i].copy()
+ if self.i < len(self.responses) - 1:
+ self.i += 1
+ else:
+ self.i = 0
+ generation = ChatGeneration(message=response)
+ return ChatResult(generations=[generation])
+
+ tool = StructuredTool.from_function(
+ lambda query: f"result for query: {query}" * 10,
+ name=str(uuid4()),
+ description="",
+ )
+
+ model = FakeFunctionChatModel(
+ responses=[
+ AIMessage(
+ content="",
+ tool_calls=[
+ {
+ "id": str(uuid4()),
+ "name": tool.name,
+ "args": {"query": str(uuid4()) * 100},
+ }
+ ],
+ id=str(uuid4()),
+ )
+ for _ in range(n_tools)
+ ]
+ + [
+ AIMessage(content="answer" * 100, id=str(uuid4())),
+ ]
+ )
+
+ return create_react_agent(model, [tool], checkpointer=checkpointer)
+
+
+if __name__ == "__main__":
+ import asyncio
+
+ import uvloop
+ from langgraph.checkpoint.memory import InMemorySaver
+
+ graph = react_agent(100, checkpointer=InMemorySaver())
+ input = {"messages": [HumanMessage("hi?")]}
+ config = {"configurable": {"thread_id": "1"}, "recursion_limit": 20000000000}
+
+ async def run():
+ len([c async for c in graph.astream(input, config=config)])
+
+ uvloop.install()
+ asyncio.run(run())
diff --git a/langgraph/source/libs/langgraph/bench/sequential.py b/langgraph/source/libs/langgraph/bench/sequential.py
new file mode 100644
index 0000000000000000000000000000000000000000..886f51ba3118beb04bbe0978b5c87cf7f804ac14
--- /dev/null
+++ b/langgraph/source/libs/langgraph/bench/sequential.py
@@ -0,0 +1,48 @@
+"""Create a sequential no-op graph consisting of a few hundred nodes."""
+
+from langgraph._internal._runnable import RunnableCallable
+from langgraph.graph import MessagesState, StateGraph
+
+
+def create_sequential(number_nodes: int) -> StateGraph:
+ """Create a sequential no-op graph consisting of a few hundred nodes."""
+ builder = StateGraph(MessagesState)
+
+ def noop(state: MessagesState) -> None:
+ """No-op function."""
+ pass
+
+ async def anoop(state: MessagesState) -> None:
+ """No-op function."""
+ pass
+
+ prev_node = "__start__"
+
+ for i in range(number_nodes):
+ name = f"node_{i}"
+ builder.add_node(name, RunnableCallable(noop, anoop))
+ builder.add_edge(prev_node, name)
+ prev_node = name
+
+ builder.add_edge(prev_node, "__end__")
+ return builder
+
+
+if __name__ == "__main__":
+ import asyncio
+ import time
+
+ import uvloop
+
+ graph = create_sequential(3000).compile()
+ input = {"messages": []} # Empty list of messages
+ config = {"recursion_limit": 20000000000}
+
+ async def run():
+ len([c async for c in graph.astream(input, config=config)])
+
+ uvloop.install()
+ start = time.time()
+ asyncio.run(run())
+ end = time.time()
+ print(f"Time taken: {end - start:.4f} seconds")
diff --git a/langgraph/source/libs/langgraph/bench/wide_dict.py b/langgraph/source/libs/langgraph/bench/wide_dict.py
new file mode 100644
index 0000000000000000000000000000000000000000..19b41148038c5df1ca5b1e317434be6c2e427062
--- /dev/null
+++ b/langgraph/source/libs/langgraph/bench/wide_dict.py
@@ -0,0 +1,151 @@
+import operator
+from collections.abc import Sequence
+from functools import partial
+from random import choice
+from typing import Annotated
+
+from typing_extensions import TypedDict
+
+from langgraph.constants import END, START
+from langgraph.graph.state import StateGraph
+
+
+def wide_dict(n: int) -> StateGraph:
+ class State(TypedDict):
+ messages: Annotated[list, operator.add]
+ trigger_events: Annotated[list, operator.add]
+ """The external events that are converted by the graph."""
+ primary_issue_medium: Annotated[str, lambda x, y: y or x]
+ autoresponse: Annotated[dict | None, lambda _, y: y] # Always overwrite
+ issue: Annotated[dict | None, lambda x, y: y if y else x]
+ relevant_rules: list[dict] | None
+ """SOPs fetched from the rulebook that are relevant to the current conversation."""
+ memory_docs: list[dict] | None
+ """Memory docs fetched from the memory service that are relevant to the current conversation."""
+ categorizations: Annotated[list[dict], operator.add]
+ """The issue categorizations auto-generated by the AI."""
+ responses: Annotated[list[dict], operator.add]
+ """The draft responses recommended by the AI."""
+
+ user_info: Annotated[dict | None, lambda x, y: y if y is not None else x]
+ """The current user state (by email)."""
+ crm_info: Annotated[dict | None, lambda x, y: y if y is not None else x]
+ """The CRM information for organization the current user is from."""
+ email_thread_id: Annotated[str | None, lambda x, y: y if y is not None else x]
+ """The current email thread ID."""
+ slack_participants: Annotated[dict, operator.or_]
+ """The growing list of current slack participants."""
+ bot_id: str | None
+ """The ID of the bot user in the slack channel."""
+ notified_assignees: Annotated[dict, operator.or_]
+
+ list_fields = {
+ "messages",
+ "trigger_events",
+ "categorizations",
+ "responses",
+ "memory_docs",
+ "relevant_rules",
+ }
+ dict_fields = {
+ "user_info",
+ "crm_info",
+ "slack_participants",
+ "notified_assignees",
+ "autoresponse",
+ "issue",
+ }
+
+ def read_write(read: str, write: Sequence[str], input: State) -> dict:
+ val = input.get(read)
+ val = {val: val} if isinstance(val, str) else val
+ val_single = val[-1] if isinstance(val, list) else val
+ val_list = val if isinstance(val, list) else [val]
+ return {
+ k: val_list
+ if k in list_fields
+ else val_single
+ if k in dict_fields
+ else "".join(choice("abcdefghijklmnopqrstuvwxyz") for _ in range(n))
+ for k in write
+ }
+
+ builder = StateGraph(State)
+ builder.add_edge(START, "one")
+ builder.add_node(
+ "one",
+ partial(read_write, "messages", ["trigger_events", "primary_issue_medium"]),
+ )
+ builder.add_edge("one", "two")
+ builder.add_node(
+ "two",
+ partial(read_write, "trigger_events", ["autoresponse", "issue"]),
+ )
+ builder.add_edge("two", "three")
+ builder.add_edge("two", "four")
+ builder.add_node(
+ "three",
+ partial(read_write, "autoresponse", ["relevant_rules"]),
+ )
+ builder.add_node(
+ "four",
+ partial(
+ read_write,
+ "trigger_events",
+ ["categorizations", "responses", "memory_docs"],
+ ),
+ )
+ builder.add_node(
+ "five",
+ partial(
+ read_write,
+ "categorizations",
+ [
+ "user_info",
+ "crm_info",
+ "email_thread_id",
+ "slack_participants",
+ "bot_id",
+ "notified_assignees",
+ ],
+ ),
+ )
+ builder.add_edge(["three", "four"], "five")
+ builder.add_edge("five", "six")
+ builder.add_node(
+ "six",
+ partial(read_write, "responses", ["messages"]),
+ )
+ builder.add_conditional_edges(
+ "six", lambda state: END if len(state["messages"]) > n else "one"
+ )
+
+ return builder
+
+
+if __name__ == "__main__":
+ import asyncio
+
+ import uvloop
+ from langgraph.checkpoint.memory import InMemorySaver
+
+ graph = wide_dict(1000).compile(checkpointer=InMemorySaver())
+ input = {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(50)
+ }
+ for i in range(50)
+ }
+ ]
+ }
+ config = {"configurable": {"thread_id": "1"}, "recursion_limit": 20000000000}
+
+ async def run():
+ async for c in graph.astream(input, config=config):
+ print(c.keys())
+
+ uvloop.install()
+ asyncio.run(run())
diff --git a/langgraph/source/libs/langgraph/bench/wide_state.py b/langgraph/source/libs/langgraph/bench/wide_state.py
new file mode 100644
index 0000000000000000000000000000000000000000..218655f5fbd5a2f22494992232aa6aa3e4a88283
--- /dev/null
+++ b/langgraph/source/libs/langgraph/bench/wide_state.py
@@ -0,0 +1,163 @@
+import operator
+from collections.abc import Sequence
+from dataclasses import dataclass, field
+from functools import partial
+from random import choice
+from typing import Annotated
+
+from langgraph.constants import END, START
+from langgraph.graph.state import StateGraph
+
+
+def wide_state(n: int) -> StateGraph:
+ @dataclass(kw_only=True)
+ class State:
+ messages: Annotated[list, operator.add] = field(default_factory=list)
+ trigger_events: Annotated[list, operator.add] = field(default_factory=list)
+ """The external events that are converted by the graph."""
+ primary_issue_medium: Annotated[str, lambda x, y: y or x] = field(
+ default="email"
+ )
+ autoresponse: Annotated[dict | None, lambda _, y: y] = field(
+ default=None
+ ) # Always overwrite
+ issue: Annotated[dict | None, lambda x, y: y if y else x] = field(default=None)
+ relevant_rules: list[dict] | None = field(default=None)
+ """SOPs fetched from the rulebook that are relevant to the current conversation."""
+ memory_docs: list[dict] | None = field(default=None)
+ """Memory docs fetched from the memory service that are relevant to the current conversation."""
+ categorizations: Annotated[list[dict], operator.add] = field(
+ default_factory=list
+ )
+ """The issue categorizations auto-generated by the AI."""
+ responses: Annotated[list[dict], operator.add] = field(default_factory=list)
+ """The draft responses recommended by the AI."""
+
+ user_info: Annotated[dict | None, lambda x, y: y if y is not None else x] = (
+ field(default=None)
+ )
+ """The current user state (by email)."""
+ crm_info: Annotated[dict | None, lambda x, y: y if y is not None else x] = (
+ field(default=None)
+ )
+ """The CRM information for organization the current user is from."""
+ email_thread_id: Annotated[
+ str | None, lambda x, y: y if y is not None else x
+ ] = field(default=None)
+ """The current email thread ID."""
+ slack_participants: Annotated[dict, operator.or_] = field(default_factory=dict)
+ """The growing list of current slack participants."""
+ bot_id: str | None = field(default=None)
+ """The ID of the bot user in the slack channel."""
+ notified_assignees: Annotated[dict, operator.or_] = field(default_factory=dict)
+
+ list_fields = {
+ "messages",
+ "trigger_events",
+ "categorizations",
+ "responses",
+ "memory_docs",
+ "relevant_rules",
+ }
+ dict_fields = {
+ "user_info",
+ "crm_info",
+ "slack_participants",
+ "notified_assignees",
+ "autoresponse",
+ "issue",
+ }
+
+ def read_write(read: str, write: Sequence[str], input: State) -> dict:
+ val = getattr(input, read)
+ val = {val: val} if isinstance(val, str) else val
+ val_single = val[-1] if isinstance(val, list) else val
+ val_list = val if isinstance(val, list) else [val]
+ return {
+ k: val_list
+ if k in list_fields
+ else val_single
+ if k in dict_fields
+ else "".join(choice("abcdefghijklmnopqrstuvwxyz") for _ in range(n))
+ for k in write
+ }
+
+ builder = StateGraph(State)
+ builder.add_edge(START, "one")
+ builder.add_node(
+ "one",
+ partial(read_write, "messages", ["trigger_events", "primary_issue_medium"]),
+ )
+ builder.add_edge("one", "two")
+ builder.add_node(
+ "two",
+ partial(read_write, "trigger_events", ["autoresponse", "issue"]),
+ )
+ builder.add_edge("two", "three")
+ builder.add_edge("two", "four")
+ builder.add_node(
+ "three",
+ partial(read_write, "autoresponse", ["relevant_rules"]),
+ )
+ builder.add_node(
+ "four",
+ partial(
+ read_write,
+ "trigger_events",
+ ["categorizations", "responses", "memory_docs"],
+ ),
+ )
+ builder.add_node(
+ "five",
+ partial(
+ read_write,
+ "categorizations",
+ [
+ "user_info",
+ "crm_info",
+ "email_thread_id",
+ "slack_participants",
+ "bot_id",
+ "notified_assignees",
+ ],
+ ),
+ )
+ builder.add_edge(["three", "four"], "five")
+ builder.add_edge("five", "six")
+ builder.add_node(
+ "six",
+ partial(read_write, "responses", ["messages"]),
+ )
+ builder.add_conditional_edges(
+ "six", lambda state: END if len(state.messages) > n else "one"
+ )
+
+ return builder
+
+
+if __name__ == "__main__":
+ import asyncio
+
+ import uvloop
+ from langgraph.checkpoint.memory import InMemorySaver
+
+ graph = wide_state(1000).compile(checkpointer=InMemorySaver())
+ input = {
+ "messages": [
+ {
+ str(i) * 10: {
+ str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5
+ for j in range(50)
+ }
+ for i in range(50)
+ }
+ ]
+ }
+ config = {"configurable": {"thread_id": "1"}, "recursion_limit": 20000000000}
+
+ async def run():
+ async for c in graph.astream(input, config=config):
+ print(c.keys())
+
+ uvloop.install()
+ asyncio.run(run())
diff --git a/langgraph/source/libs/langgraph/langgraph/__init__.py b/langgraph/source/libs/langgraph/langgraph/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a96afc6ff09d58a702b76e3f7dd412fe975e26
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/__init__.py
@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-
diff --git a/langgraph/source/libs/langgraph/langgraph/_internal/__init__.py b/langgraph/source/libs/langgraph/langgraph/_internal/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..2e71cdc23604618ae999e82ee1a796b78543ddc4
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/_internal/__init__.py
@@ -0,0 +1,4 @@
+"""Internal modules for LangGraph.
+
+This module is not part of the public API, and thus stability is not guaranteed.
+"""
diff --git a/langgraph/source/libs/langgraph/langgraph/_internal/_cache.py b/langgraph/source/libs/langgraph/langgraph/_internal/_cache.py
new file mode 100644
index 0000000000000000000000000000000000000000..c9c451474b96509db622e1197a4a95d9a21ca4a2
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/_internal/_cache.py
@@ -0,0 +1,31 @@
+from __future__ import annotations
+
+from collections.abc import Hashable, Mapping, Sequence
+from typing import Any
+
+
+def _freeze(obj: Any, depth: int = 10) -> Hashable:
+ if isinstance(obj, Hashable) or depth <= 0:
+ # already hashable, no need to freeze
+ return obj
+ elif isinstance(obj, Mapping):
+ # sort keys so {"a":1,"b":2} == {"b":2,"a":1}
+ return tuple(sorted((k, _freeze(v, depth - 1)) for k, v in obj.items()))
+ elif isinstance(obj, Sequence):
+ return tuple(_freeze(x, depth - 1) for x in obj)
+ # numpy / pandas etc. can provide their own .tobytes()
+ elif hasattr(obj, "tobytes"):
+ return (
+ type(obj).__name__,
+ obj.tobytes(),
+ obj.shape if hasattr(obj, "shape") else None,
+ )
+ return obj # strings, ints, dataclasses with frozen=True, etc.
+
+
+def default_cache_key(*args: Any, **kwargs: Any) -> str | bytes:
+ """Default cache key function that uses the arguments and keyword arguments to generate a hashable key."""
+ import pickle
+
+ # protocol 5 strikes a good balance between speed and size
+ return pickle.dumps((_freeze(args), _freeze(kwargs)), protocol=5, fix_imports=False)
diff --git a/langgraph/source/libs/langgraph/langgraph/_internal/_config.py b/langgraph/source/libs/langgraph/langgraph/_internal/_config.py
new file mode 100644
index 0000000000000000000000000000000000000000..36b285dca5f3d1ad16357694d965392781988baa
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/_internal/_config.py
@@ -0,0 +1,329 @@
+from __future__ import annotations
+
+from collections import ChainMap
+from collections.abc import Mapping, Sequence
+from os import getenv
+from typing import Any, cast
+
+from langchain_core.callbacks import (
+ AsyncCallbackManager,
+ BaseCallbackManager,
+ CallbackManager,
+ Callbacks,
+)
+from langchain_core.runnables import RunnableConfig
+from langchain_core.runnables.config import (
+ CONFIG_KEYS,
+ COPIABLE_KEYS,
+ var_child_runnable_config,
+)
+from langgraph.checkpoint.base import CheckpointMetadata
+
+from langgraph._internal._constants import (
+ CONF,
+ CONFIG_KEY_CHECKPOINT_ID,
+ CONFIG_KEY_CHECKPOINT_MAP,
+ CONFIG_KEY_CHECKPOINT_NS,
+ NS_END,
+ NS_SEP,
+)
+
+DEFAULT_RECURSION_LIMIT = int(getenv("LANGGRAPH_DEFAULT_RECURSION_LIMIT", "10000"))
+
+
+def recast_checkpoint_ns(ns: str) -> str:
+ """Remove task IDs from checkpoint namespace.
+
+ Args:
+ ns: The checkpoint namespace with task IDs.
+
+ Returns:
+ str: The checkpoint namespace without task IDs.
+ """
+ return NS_SEP.join(
+ part.split(NS_END)[0] for part in ns.split(NS_SEP) if not part.isdigit()
+ )
+
+
+def patch_configurable(
+ config: RunnableConfig | None, patch: dict[str, Any]
+) -> RunnableConfig:
+ if config is None:
+ return {CONF: patch}
+ elif CONF not in config:
+ return {**config, CONF: patch}
+ else:
+ return {**config, CONF: {**config[CONF], **patch}}
+
+
+def patch_checkpoint_map(
+ config: RunnableConfig | None, metadata: CheckpointMetadata | None
+) -> RunnableConfig:
+ if config is None:
+ return config
+ elif parents := (metadata.get("parents") if metadata else None):
+ conf = config[CONF]
+ return patch_configurable(
+ config,
+ {
+ CONFIG_KEY_CHECKPOINT_MAP: {
+ **parents,
+ conf[CONFIG_KEY_CHECKPOINT_NS]: conf[CONFIG_KEY_CHECKPOINT_ID],
+ },
+ },
+ )
+ else:
+ return config
+
+
+def merge_configs(*configs: RunnableConfig | None) -> RunnableConfig:
+ """Merge multiple configs into one.
+
+ Args:
+ *configs: The configs to merge.
+
+ Returns:
+ RunnableConfig: The merged config.
+ """
+ base: RunnableConfig = {}
+ # Even though the keys aren't literals, this is correct
+ # because both dicts are the same type
+ for config in configs:
+ if config is None:
+ continue
+ for key, value in config.items():
+ if not value:
+ continue
+ if key == "metadata":
+ if base_value := base.get(key):
+ base[key] = {**base_value, **value} # type: ignore
+ else:
+ base[key] = value # type: ignore[literal-required]
+ elif key == "tags":
+ if base_value := base.get(key):
+ base[key] = [*base_value, *value] # type: ignore
+ else:
+ base[key] = value # type: ignore[literal-required]
+ elif key == CONF:
+ if base_value := base.get(key):
+ base[key] = {**base_value, **value} # type: ignore[dict-item]
+ else:
+ base[key] = value
+ elif key == "callbacks":
+ base_callbacks = base.get("callbacks")
+ # callbacks can be either None, list[handler] or manager
+ # so merging two callbacks values has 6 cases
+ if isinstance(value, list):
+ if base_callbacks is None:
+ base["callbacks"] = value.copy()
+ elif isinstance(base_callbacks, list):
+ base["callbacks"] = base_callbacks + value
+ else:
+ # base_callbacks is a manager
+ mngr = base_callbacks.copy()
+ for callback in value:
+ mngr.add_handler(callback, inherit=True)
+ base["callbacks"] = mngr
+ elif isinstance(value, BaseCallbackManager):
+ # value is a manager
+ if base_callbacks is None:
+ base["callbacks"] = value.copy()
+ elif isinstance(base_callbacks, list):
+ mngr = value.copy()
+ for callback in base_callbacks:
+ mngr.add_handler(callback, inherit=True)
+ base["callbacks"] = mngr
+ else:
+ # base_callbacks is also a manager
+ base["callbacks"] = base_callbacks.merge(value)
+ else:
+ raise NotImplementedError
+ elif key == "recursion_limit":
+ if config["recursion_limit"] != DEFAULT_RECURSION_LIMIT:
+ base["recursion_limit"] = config["recursion_limit"]
+ else:
+ base[key] = config[key] # type: ignore[literal-required]
+ if CONF not in base:
+ base[CONF] = {}
+ return base
+
+
+def patch_config(
+ config: RunnableConfig | None,
+ *,
+ callbacks: Callbacks = None,
+ recursion_limit: int | None = None,
+ max_concurrency: int | None = None,
+ run_name: str | None = None,
+ configurable: dict[str, Any] | None = None,
+) -> RunnableConfig:
+ """Patch a config with new values.
+
+ Args:
+ config: The config to patch.
+ callbacks: The callbacks to set.
+ recursion_limit: The recursion limit to set.
+ max_concurrency: The max number of concurrent steps to run, which also applies to parallelized steps.
+ run_name: The run name to set.
+ configurable: The configurable to set.
+
+ Returns:
+ RunnableConfig: The patched config.
+ """
+ config = config.copy() if config is not None else {}
+ if callbacks is not None:
+ # If we're replacing callbacks, we need to unset run_name
+ # As that should apply only to the same run as the original callbacks
+ config["callbacks"] = callbacks
+ if "run_name" in config:
+ del config["run_name"]
+ if "run_id" in config:
+ del config["run_id"]
+ if recursion_limit is not None:
+ config["recursion_limit"] = recursion_limit
+ if max_concurrency is not None:
+ config["max_concurrency"] = max_concurrency
+ if run_name is not None:
+ config["run_name"] = run_name
+ if configurable is not None:
+ config[CONF] = {**config.get(CONF, {}), **configurable}
+ return config
+
+
+def get_callback_manager_for_config(
+ config: RunnableConfig, tags: Sequence[str] | None = None
+) -> CallbackManager:
+ """Get a callback manager for a config.
+
+ Args:
+ config: The config.
+
+ Returns:
+ CallbackManager: The callback manager.
+ """
+ from langchain_core.callbacks.manager import CallbackManager
+
+ # merge tags
+ all_tags = config.get("tags")
+ if all_tags is not None and tags is not None:
+ all_tags = [*all_tags, *tags]
+ elif tags is not None:
+ all_tags = list(tags)
+ # use existing callbacks if they exist
+ if (callbacks := config.get("callbacks")) and isinstance(
+ callbacks, CallbackManager
+ ):
+ if all_tags:
+ callbacks.add_tags(all_tags)
+ if metadata := config.get("metadata"):
+ callbacks.add_metadata(metadata)
+ return callbacks
+ else:
+ # otherwise create a new manager
+ return CallbackManager.configure(
+ inheritable_callbacks=config.get("callbacks"),
+ inheritable_tags=all_tags,
+ inheritable_metadata=config.get("metadata"),
+ )
+
+
+def get_async_callback_manager_for_config(
+ config: RunnableConfig,
+ tags: Sequence[str] | None = None,
+) -> AsyncCallbackManager:
+ """Get an async callback manager for a config.
+
+ Args:
+ config: The config.
+
+ Returns:
+ AsyncCallbackManager: The async callback manager.
+ """
+ from langchain_core.callbacks.manager import AsyncCallbackManager
+
+ # merge tags
+ all_tags = config.get("tags")
+ if all_tags is not None and tags is not None:
+ all_tags = [*all_tags, *tags]
+ elif tags is not None:
+ all_tags = list(tags)
+ # use existing callbacks if they exist
+ if (callbacks := config.get("callbacks")) and isinstance(
+ callbacks, AsyncCallbackManager
+ ):
+ if all_tags:
+ callbacks.add_tags(all_tags)
+ if metadata := config.get("metadata"):
+ callbacks.add_metadata(metadata)
+ return callbacks
+ else:
+ # otherwise create a new manager
+ return AsyncCallbackManager.configure(
+ inheritable_callbacks=config.get("callbacks"),
+ inheritable_tags=all_tags,
+ inheritable_metadata=config.get("metadata"),
+ )
+
+
+def _is_not_empty(value: Any) -> bool:
+ if isinstance(value, (list, tuple, dict)):
+ return len(value) > 0
+ else:
+ return value is not None
+
+
+def ensure_config(*configs: RunnableConfig | None) -> RunnableConfig:
+ """Return a config with all keys, merging any provided configs.
+
+ Args:
+ *configs: Configs to merge before ensuring defaults.
+
+ Returns:
+ RunnableConfig: The merged and ensured config.
+ """
+ empty = RunnableConfig(
+ tags=[],
+ metadata=ChainMap(),
+ callbacks=None,
+ recursion_limit=DEFAULT_RECURSION_LIMIT,
+ configurable={},
+ )
+ if var_config := var_child_runnable_config.get():
+ empty.update(
+ {
+ k: v.copy() if k in COPIABLE_KEYS else v # type: ignore[attr-defined]
+ for k, v in var_config.items()
+ if _is_not_empty(v)
+ },
+ )
+ for config in configs:
+ if config is None:
+ continue
+ for k, v in config.items():
+ if _is_not_empty(v) and k in CONFIG_KEYS:
+ if k == CONF:
+ empty[k] = cast(dict, v).copy()
+ else:
+ empty[k] = v # type: ignore[literal-required]
+ for k, v in config.items():
+ if _is_not_empty(v) and k not in CONFIG_KEYS:
+ empty[CONF][k] = v
+ _empty_metadata = empty["metadata"]
+ for key, value in empty[CONF].items():
+ if _exclude_as_metadata(key, value, _empty_metadata):
+ continue
+ _empty_metadata[key] = value
+ return empty
+
+
+_OMIT = ("key", "token", "secret", "password", "auth")
+
+
+def _exclude_as_metadata(key: str, value: Any, metadata: Mapping[str, Any]) -> bool:
+ key_lower = key.casefold()
+ return (
+ key.startswith("__")
+ or not isinstance(value, (str, int, float, bool))
+ or key in metadata
+ or any(substr in key_lower for substr in _OMIT)
+ )
diff --git a/langgraph/source/libs/langgraph/langgraph/_internal/_constants.py b/langgraph/source/libs/langgraph/langgraph/_internal/_constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..b538502c424b56b0ec98e65192da3c56ab6f0607
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/_internal/_constants.py
@@ -0,0 +1,112 @@
+"""Constants used for Pregel operations."""
+
+import sys
+from typing import Literal, cast
+
+# --- Reserved write keys ---
+INPUT = sys.intern("__input__")
+# for values passed as input to the graph
+INTERRUPT = sys.intern("__interrupt__")
+# for dynamic interrupts raised by nodes
+RESUME = sys.intern("__resume__")
+# for values passed to resume a node after an interrupt
+ERROR = sys.intern("__error__")
+# for errors raised by nodes
+NO_WRITES = sys.intern("__no_writes__")
+# marker to signal node didn't write anything
+TASKS = sys.intern("__pregel_tasks")
+# for Send objects returned by nodes/edges, corresponds to PUSH below
+RETURN = sys.intern("__return__")
+# for writes of a task where we simply record the return value
+PREVIOUS = sys.intern("__previous__")
+# the implicit branch that handles each node's Control values
+
+
+# --- Reserved cache namespaces ---
+CACHE_NS_WRITES = sys.intern("__pregel_ns_writes")
+# cache namespace for node writes
+
+# --- Reserved config.configurable keys ---
+CONFIG_KEY_SEND = sys.intern("__pregel_send")
+# holds the `write` function that accepts writes to state/edges/reserved keys
+CONFIG_KEY_READ = sys.intern("__pregel_read")
+# holds the `read` function that returns a copy of the current state
+CONFIG_KEY_CALL = sys.intern("__pregel_call")
+# holds the `call` function that accepts a node/func, args and returns a future
+CONFIG_KEY_CHECKPOINTER = sys.intern("__pregel_checkpointer")
+# holds a `BaseCheckpointSaver` passed from parent graph to child graphs
+CONFIG_KEY_STREAM = sys.intern("__pregel_stream")
+# holds a `StreamProtocol` passed from parent graph to child graphs
+CONFIG_KEY_CACHE = sys.intern("__pregel_cache")
+# holds a `BaseCache` made available to subgraphs
+CONFIG_KEY_RESUMING = sys.intern("__pregel_resuming")
+# holds a boolean indicating if subgraphs should resume from a previous checkpoint
+CONFIG_KEY_TASK_ID = sys.intern("__pregel_task_id")
+# holds the task ID for the current task
+CONFIG_KEY_THREAD_ID = sys.intern("thread_id")
+# holds the thread ID for the current invocation
+CONFIG_KEY_CHECKPOINT_MAP = sys.intern("checkpoint_map")
+# holds a mapping of checkpoint_ns -> checkpoint_id for parent graphs
+CONFIG_KEY_CHECKPOINT_ID = sys.intern("checkpoint_id")
+# holds the current checkpoint_id, if any
+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")
+# holds a callback to be called when a node is finished
+CONFIG_KEY_SCRATCHPAD = sys.intern("__pregel_scratchpad")
+# holds a mutable dict for temporary storage scoped to the current task
+CONFIG_KEY_RUNNER_SUBMIT = sys.intern("__pregel_runner_submit")
+# holds a function that receives tasks from runner, executes them and returns results
+CONFIG_KEY_DURABILITY = sys.intern("__pregel_durability")
+# holds the durability mode, one of "sync", "async", or "exit"
+CONFIG_KEY_RUNTIME = sys.intern("__pregel_runtime")
+# holds a `Runtime` instance with context, store, stream writer, etc.
+CONFIG_KEY_RESUME_MAP = sys.intern("__pregel_resume_map")
+# holds a mapping of task ns -> resume value for resuming tasks
+
+# --- Other constants ---
+PUSH = sys.intern("__pregel_push")
+# denotes push-style tasks, ie. those created by Send objects
+PULL = sys.intern("__pregel_pull")
+# denotes pull-style tasks, ie. those triggered by edges
+NS_SEP = sys.intern("|")
+# for checkpoint_ns, separates each level (ie. graph|subgraph|subsubgraph)
+NS_END = sys.intern(":")
+# for checkpoint_ns, for each level, separates the namespace from the task_id
+CONF = cast(Literal["configurable"], sys.intern("configurable"))
+# key for the configurable dict in RunnableConfig
+NULL_TASK_ID = sys.intern("00000000-0000-0000-0000-000000000000")
+# the task_id to use for writes that are not associated with a task
+OVERWRITE = sys.intern("__overwrite__")
+# dict key for the overwrite value, used as `{'__overwrite__': value}`
+
+# redefined to avoid circular import with langgraph.constants
+_TAG_HIDDEN = sys.intern("langsmith:hidden")
+
+RESERVED = {
+ _TAG_HIDDEN,
+ # reserved write keys
+ INPUT,
+ INTERRUPT,
+ RESUME,
+ ERROR,
+ NO_WRITES,
+ # reserved config.configurable keys
+ CONFIG_KEY_SEND,
+ CONFIG_KEY_READ,
+ CONFIG_KEY_CHECKPOINTER,
+ CONFIG_KEY_STREAM,
+ CONFIG_KEY_CHECKPOINT_MAP,
+ CONFIG_KEY_RESUMING,
+ CONFIG_KEY_TASK_ID,
+ CONFIG_KEY_CHECKPOINT_MAP,
+ CONFIG_KEY_CHECKPOINT_ID,
+ CONFIG_KEY_CHECKPOINT_NS,
+ CONFIG_KEY_RESUME_MAP,
+ # other constants
+ PUSH,
+ PULL,
+ NS_SEP,
+ NS_END,
+ CONF,
+}
diff --git a/langgraph/source/libs/langgraph/langgraph/_internal/_fields.py b/langgraph/source/libs/langgraph/langgraph/_internal/_fields.py
new file mode 100644
index 0000000000000000000000000000000000000000..7ada8dfa0cb053a98b5c1ce9d9dca3dcedda2775
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/_internal/_fields.py
@@ -0,0 +1,213 @@
+from __future__ import annotations
+
+import dataclasses
+import types
+import weakref
+from collections.abc import Generator, Sequence
+from typing import Annotated, Any, Optional, Union, get_origin, get_type_hints
+
+from pydantic import BaseModel
+from typing_extensions import NotRequired, ReadOnly, Required
+
+from langgraph._internal._typing import MISSING
+
+
+def _is_optional_type(type_: Any) -> bool:
+ """Check if a type is Optional."""
+
+ # Handle new union syntax (PEP 604): str | None
+ if isinstance(type_, types.UnionType):
+ return any(
+ arg is type(None) or _is_optional_type(arg) for arg in type_.__args__
+ )
+
+ if hasattr(type_, "__origin__") and hasattr(type_, "__args__"):
+ origin = get_origin(type_)
+ if origin is Optional:
+ return True
+ if origin is Union:
+ return any(
+ arg is type(None) or _is_optional_type(arg) for arg in type_.__args__
+ )
+ if origin is Annotated:
+ return _is_optional_type(type_.__args__[0])
+ return origin is None
+ if hasattr(type_, "__bound__") and type_.__bound__ is not None:
+ return _is_optional_type(type_.__bound__)
+ return type_ is None
+
+
+def _is_required_type(type_: Any) -> bool | None:
+ """Check if an annotation is marked as Required/NotRequired.
+
+ Returns:
+ - True if required
+ - False if not required
+ - None if not annotated with either
+ """
+ origin = get_origin(type_)
+ if origin is Required:
+ return True
+ if origin is NotRequired:
+ return False
+ if origin is Annotated or getattr(origin, "__args__", None):
+ # See https://typing.readthedocs.io/en/latest/spec/typeddict.html#interaction-with-annotated
+ return _is_required_type(type_.__args__[0])
+ return None
+
+
+def _is_readonly_type(type_: Any) -> bool:
+ """Check if an annotation is marked as ReadOnly.
+
+ Returns:
+ - True if is read only
+ - False if not read only
+ """
+
+ # See: https://typing.readthedocs.io/en/latest/spec/typeddict.html#typing-readonly-type-qualifier
+ origin = get_origin(type_)
+ if origin is Annotated:
+ return _is_readonly_type(type_.__args__[0])
+ if origin is ReadOnly:
+ return True
+ return False
+
+
+_DEFAULT_KEYS: frozenset[str] = frozenset()
+
+
+def get_field_default(name: str, type_: Any, schema: type[Any]) -> Any:
+ """Determine the default value for a field in a state schema.
+
+ This is based on:
+ If TypedDict:
+ - Required/NotRequired
+ - total=False -> everything optional
+ - Type annotation (Optional/Union[None])
+ """
+ optional_keys = getattr(schema, "__optional_keys__", _DEFAULT_KEYS)
+ irq = _is_required_type(type_)
+ if name in optional_keys:
+ # Either total=False or explicit NotRequired.
+ # No type annotation trumps this.
+ if irq:
+ # Unless it's earlier versions of python & explicit Required
+ return ...
+ return None
+ if irq is not None:
+ if irq:
+ # Handle Required[]
+ # (we already handled NotRequired and total=False)
+ return ...
+ # Handle NotRequired[] for earlier versions of python
+ return None
+ if dataclasses.is_dataclass(schema):
+ field_info = next(
+ (f for f in dataclasses.fields(schema) if f.name == name), None
+ )
+ if field_info:
+ if (
+ field_info.default is not dataclasses.MISSING
+ and field_info.default is not ...
+ ):
+ return field_info.default
+ elif field_info.default_factory is not dataclasses.MISSING:
+ return field_info.default_factory()
+ # Note, we ignore ReadOnly attributes,
+ # as they don't make much sense. (we don't care if you mutate the state in your node)
+ # and mutating state in your node has no effect on our graph state.
+ # Base case is the annotation
+ if _is_optional_type(type_):
+ return None
+ return ...
+
+
+def get_enhanced_type_hints(
+ type: type[Any],
+) -> Generator[tuple[str, Any, Any, str | None], None, None]:
+ """Attempt to extract default values and descriptions from provided type, used for config schema."""
+ for name, typ in get_type_hints(type).items():
+ default = None
+ description = None
+
+ # Pydantic models
+ try:
+ if hasattr(type, "model_fields") and name in type.model_fields:
+ field = type.model_fields[name]
+
+ if hasattr(field, "description") and field.description is not None:
+ description = field.description
+
+ if hasattr(field, "default") and field.default is not None:
+ default = field.default
+ if (
+ hasattr(default, "__class__")
+ and getattr(default.__class__, "__name__", "")
+ == "PydanticUndefinedType"
+ ):
+ default = None
+
+ except (AttributeError, KeyError, TypeError):
+ pass
+
+ # TypedDict, dataclass
+ try:
+ if hasattr(type, "__dict__"):
+ type_dict = getattr(type, "__dict__")
+
+ if name in type_dict:
+ default = type_dict[name]
+ except (AttributeError, KeyError, TypeError):
+ pass
+
+ yield name, typ, default, description
+
+
+def get_update_as_tuples(input: Any, keys: Sequence[str]) -> list[tuple[str, Any]]:
+ """Get Pydantic state update as a list of (key, value) tuples."""
+ if isinstance(input, BaseModel):
+ keep = input.model_fields_set
+ defaults = {k: v.default for k, v in type(input).model_fields.items()}
+ else:
+ keep = None
+ defaults = {}
+
+ # NOTE: This behavior for Pydantic is somewhat inelegant,
+ # but we keep around for backwards compatibility
+ # if input is a Pydantic model, only update values
+ # that are different from the default values or in the keep set
+ return [
+ (k, value)
+ for k in keys
+ if (value := getattr(input, k, MISSING)) is not MISSING
+ and (
+ value is not None
+ or defaults.get(k, MISSING) is not None
+ or (keep is not None and k in keep)
+ )
+ ]
+
+
+ANNOTATED_KEYS_CACHE: weakref.WeakKeyDictionary[type[Any], tuple[str, ...]] = (
+ weakref.WeakKeyDictionary()
+)
+
+
+def get_cached_annotated_keys(obj: type[Any]) -> tuple[str, ...]:
+ """Return cached annotated keys for a Python class."""
+ if obj in ANNOTATED_KEYS_CACHE:
+ return ANNOTATED_KEYS_CACHE[obj]
+ if isinstance(obj, type):
+ keys: list[str] = []
+ for base in reversed(obj.__mro__):
+ ann = base.__dict__.get("__annotations__")
+ # In Python 3.14+, Pydantic models use descriptors for __annotations__
+ # so we need to fall back to getattr if __dict__.get returns None
+ if ann is None:
+ ann = getattr(base, "__annotations__", None)
+ if ann is None or isinstance(ann, types.GetSetDescriptorType):
+ continue
+ keys.extend(ann.keys())
+ return ANNOTATED_KEYS_CACHE.setdefault(obj, tuple(keys))
+ else:
+ raise TypeError(f"Expected a type, got {type(obj)}. ")
diff --git a/langgraph/source/libs/langgraph/langgraph/_internal/_future.py b/langgraph/source/libs/langgraph/langgraph/_internal/_future.py
new file mode 100644
index 0000000000000000000000000000000000000000..31c56bf4bfab0c5edec7e42e6c816acd2ad4637d
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/_internal/_future.py
@@ -0,0 +1,220 @@
+from __future__ import annotations
+
+import asyncio
+import concurrent.futures
+import contextvars
+import inspect
+import sys
+import types
+from collections.abc import Awaitable, Coroutine, Generator
+from typing import TypeVar, cast
+
+T = TypeVar("T")
+AnyFuture = asyncio.Future | concurrent.futures.Future
+
+CONTEXT_NOT_SUPPORTED = sys.version_info < (3, 11)
+EAGER_NOT_SUPPORTED = sys.version_info < (3, 12)
+
+
+def _get_loop(fut: asyncio.Future) -> asyncio.AbstractEventLoop:
+ # Tries to call Future.get_loop() if it's available.
+ # Otherwise fallbacks to using the old '_loop' property.
+ try:
+ get_loop = fut.get_loop
+ except AttributeError:
+ pass
+ else:
+ return get_loop()
+ return fut._loop
+
+
+def _convert_future_exc(exc: BaseException) -> BaseException:
+ exc_class = type(exc)
+ if exc_class is concurrent.futures.CancelledError:
+ return asyncio.CancelledError(*exc.args)
+ elif exc_class is concurrent.futures.TimeoutError:
+ return asyncio.TimeoutError(*exc.args)
+ elif exc_class is concurrent.futures.InvalidStateError:
+ return asyncio.InvalidStateError(*exc.args)
+ else:
+ return exc
+
+
+def _set_concurrent_future_state(
+ concurrent: concurrent.futures.Future,
+ source: AnyFuture,
+) -> None:
+ """Copy state from a future to a concurrent.futures.Future."""
+ assert source.done()
+ if source.cancelled():
+ concurrent.cancel()
+ if not concurrent.set_running_or_notify_cancel():
+ return
+ exception = source.exception()
+ if exception is not None:
+ concurrent.set_exception(_convert_future_exc(exception))
+ else:
+ result = source.result()
+ concurrent.set_result(result)
+
+
+def _copy_future_state(source: AnyFuture, dest: asyncio.Future) -> None:
+ """Internal helper to copy state from another Future.
+
+ The other Future may be a concurrent.futures.Future.
+ """
+ if dest.done():
+ return
+ assert source.done()
+ if dest.cancelled():
+ return
+ if source.cancelled():
+ dest.cancel()
+ else:
+ exception = source.exception()
+ if exception is not None:
+ dest.set_exception(_convert_future_exc(exception))
+ else:
+ result = source.result()
+ dest.set_result(result)
+
+
+def _chain_future(source: AnyFuture, destination: AnyFuture) -> None:
+ """Chain two futures so that when one completes, so does the other.
+
+ The result (or exception) of source will be copied to destination.
+ If destination is cancelled, source gets cancelled too.
+ Compatible with both asyncio.Future and concurrent.futures.Future.
+ """
+ if not asyncio.isfuture(source) and not isinstance(
+ source, concurrent.futures.Future
+ ):
+ raise TypeError("A future is required for source argument")
+ if not asyncio.isfuture(destination) and not isinstance(
+ destination, concurrent.futures.Future
+ ):
+ raise TypeError("A future is required for destination argument")
+ source_loop = _get_loop(source) if asyncio.isfuture(source) else None
+ dest_loop = _get_loop(destination) if asyncio.isfuture(destination) else None
+
+ def _set_state(future: AnyFuture, other: AnyFuture) -> None:
+ if asyncio.isfuture(future):
+ _copy_future_state(other, future)
+ else:
+ _set_concurrent_future_state(future, other)
+
+ def _call_check_cancel(destination: AnyFuture) -> None:
+ if destination.cancelled():
+ if source_loop is None or source_loop is dest_loop:
+ source.cancel()
+ else:
+ source_loop.call_soon_threadsafe(source.cancel)
+
+ def _call_set_state(source: AnyFuture) -> None:
+ if destination.cancelled() and dest_loop is not None and dest_loop.is_closed():
+ return
+ if dest_loop is None or dest_loop is source_loop:
+ _set_state(destination, source)
+ else:
+ if dest_loop.is_closed():
+ return
+ dest_loop.call_soon_threadsafe(_set_state, destination, source)
+
+ destination.add_done_callback(_call_check_cancel)
+ source.add_done_callback(_call_set_state)
+
+
+def chain_future(source: AnyFuture, destination: AnyFuture) -> AnyFuture:
+ # adapted from asyncio.run_coroutine_threadsafe
+ try:
+ _chain_future(source, destination)
+ return destination
+ except (SystemExit, KeyboardInterrupt):
+ raise
+ except BaseException as exc:
+ if isinstance(destination, concurrent.futures.Future):
+ if destination.set_running_or_notify_cancel():
+ destination.set_exception(exc)
+ else:
+ destination.set_exception(exc)
+ raise
+
+
+def _ensure_future(
+ coro_or_future: Coroutine[None, None, T] | Awaitable[T],
+ *,
+ loop: asyncio.AbstractEventLoop,
+ name: str | None = None,
+ context: contextvars.Context | None = None,
+ lazy: bool = True,
+) -> asyncio.Task[T]:
+ called_wrap_awaitable = False
+ if not asyncio.iscoroutine(coro_or_future):
+ if inspect.isawaitable(coro_or_future):
+ coro_or_future = cast(
+ Coroutine[None, None, T], _wrap_awaitable(coro_or_future)
+ )
+ called_wrap_awaitable = True
+ else:
+ raise TypeError(
+ "An asyncio.Future, a coroutine or an awaitable is required."
+ f" Got {type(coro_or_future).__name__} instead."
+ )
+
+ try:
+ if CONTEXT_NOT_SUPPORTED:
+ return loop.create_task(coro_or_future, name=name)
+ elif EAGER_NOT_SUPPORTED or lazy:
+ return loop.create_task(coro_or_future, name=name, context=context)
+ else:
+ return asyncio.eager_task_factory(
+ loop, coro_or_future, name=name, context=context
+ )
+ except RuntimeError:
+ if not called_wrap_awaitable:
+ coro_or_future.close()
+ raise
+
+
+@types.coroutine
+def _wrap_awaitable(awaitable: Awaitable[T]) -> Generator[None, None, T]:
+ """Helper for asyncio.ensure_future().
+
+ Wraps awaitable (an object with __await__) into a coroutine
+ that will later be wrapped in a Task by ensure_future().
+ """
+ return (yield from awaitable.__await__())
+
+
+def run_coroutine_threadsafe(
+ coro: Coroutine[None, None, T],
+ loop: asyncio.AbstractEventLoop,
+ *,
+ lazy: bool,
+ name: str | None = None,
+ context: contextvars.Context | None = None,
+) -> asyncio.Future[T]:
+ """Submit a coroutine object to a given event loop.
+
+ Return an asyncio.Future to access the result.
+ """
+
+ if asyncio._get_running_loop() is loop:
+ return _ensure_future(coro, loop=loop, name=name, context=context, lazy=lazy)
+ else:
+ future: asyncio.Future[T] = asyncio.Future(loop=loop)
+
+ def callback() -> None:
+ try:
+ chain_future(
+ _ensure_future(coro, loop=loop, name=name, context=context),
+ future,
+ )
+ except (SystemExit, KeyboardInterrupt):
+ raise
+ except BaseException as exc:
+ future.set_exception(exc)
+ raise
+
+ loop.call_soon_threadsafe(callback, context=context)
+ return future
diff --git a/langgraph/source/libs/langgraph/langgraph/_internal/_pydantic.py b/langgraph/source/libs/langgraph/langgraph/_internal/_pydantic.py
new file mode 100644
index 0000000000000000000000000000000000000000..0d93f085e1aac6eb7610c6dd060c057f3fb1dd1f
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/_internal/_pydantic.py
@@ -0,0 +1,275 @@
+from __future__ import annotations
+
+import sys
+import typing
+import warnings
+from contextlib import nullcontext
+from dataclasses import is_dataclass
+from functools import lru_cache
+from typing import (
+ Any,
+ cast,
+ overload,
+)
+
+from pydantic import (
+ BaseModel,
+ ConfigDict,
+ Field,
+ RootModel,
+)
+from pydantic import (
+ create_model as _create_model_base,
+)
+from pydantic.fields import FieldInfo
+from pydantic.json_schema import (
+ DEFAULT_REF_TEMPLATE,
+ GenerateJsonSchema,
+ JsonSchemaMode,
+)
+from typing_extensions import TypedDict
+
+
+@overload
+def get_fields(model: type[BaseModel]) -> dict[str, FieldInfo]: ...
+
+
+@overload
+def get_fields(model: BaseModel) -> dict[str, FieldInfo]: ...
+
+
+def get_fields(
+ model: type[BaseModel] | BaseModel,
+) -> dict[str, FieldInfo]:
+ """Get the field names of a Pydantic model."""
+ if hasattr(model, "model_fields"):
+ return model.model_fields
+
+ if hasattr(model, "__fields__"):
+ return model.__fields__
+ msg = f"Expected a Pydantic model. Got {type(model)}"
+ raise TypeError(msg)
+
+
+_SchemaConfig = ConfigDict(
+ arbitrary_types_allowed=True, frozen=True, protected_namespaces=()
+)
+
+NO_DEFAULT = object()
+
+
+def _create_root_model(
+ name: str,
+ type_: Any,
+ module_name: str | None = None,
+ default_: object = NO_DEFAULT,
+) -> type[BaseModel]:
+ """Create a base class."""
+
+ def schema(
+ cls: type[BaseModel],
+ by_alias: bool = True, # noqa: FBT001,FBT002
+ ref_template: str = DEFAULT_REF_TEMPLATE,
+ ) -> dict[str, Any]:
+ # Complains about schema not being defined in superclass
+ schema_ = super(cls, cls).schema( # type: ignore[misc]
+ by_alias=by_alias, ref_template=ref_template
+ )
+ schema_["title"] = name
+ return schema_
+
+ def model_json_schema(
+ cls: type[BaseModel],
+ by_alias: bool = True, # noqa: FBT001,FBT002
+ ref_template: str = DEFAULT_REF_TEMPLATE,
+ schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
+ mode: JsonSchemaMode = "validation",
+ ) -> dict[str, Any]:
+ # Complains about model_json_schema not being defined in superclass
+ schema_ = super(cls, cls).model_json_schema( # type: ignore[misc]
+ by_alias=by_alias,
+ ref_template=ref_template,
+ schema_generator=schema_generator,
+ mode=mode,
+ )
+ schema_["title"] = name
+ return schema_
+
+ base_class_attributes = {
+ "__annotations__": {"root": type_},
+ "model_config": ConfigDict(arbitrary_types_allowed=True),
+ "schema": classmethod(schema),
+ "model_json_schema": classmethod(model_json_schema),
+ "__module__": module_name or "langchain_core.runnables.utils",
+ }
+
+ if default_ is not NO_DEFAULT:
+ base_class_attributes["root"] = default_
+ with warnings.catch_warnings():
+ custom_root_type = type(name, (RootModel,), base_class_attributes)
+ return cast("type[BaseModel]", custom_root_type)
+
+
+@lru_cache(maxsize=256)
+def _create_root_model_cached(
+ model_name: str,
+ type_: Any,
+ *,
+ module_name: str | None = None,
+ default_: object = NO_DEFAULT,
+) -> type[BaseModel]:
+ return _create_root_model(
+ model_name, type_, default_=default_, module_name=module_name
+ )
+
+
+@lru_cache(maxsize=256)
+def _create_model_cached(
+ model_name: str,
+ /,
+ **field_definitions: Any,
+) -> type[BaseModel]:
+ return _create_model_base(
+ model_name,
+ __config__=_SchemaConfig,
+ **_remap_field_definitions(field_definitions),
+ )
+
+
+# Reserved names should capture all the `public` names / methods that are
+# used by BaseModel internally. This will keep the reserved names up-to-date.
+# For reference, the reserved names are:
+# "construct", "copy", "dict", "from_orm", "json", "parse_file", "parse_obj",
+# "parse_raw", "schema", "schema_json", "update_forward_refs", "validate",
+# "model_computed_fields", "model_config", "model_construct", "model_copy",
+# "model_dump", "model_dump_json", "model_extra", "model_fields",
+# "model_fields_set", "model_json_schema", "model_parametrized_name",
+# "model_post_init", "model_rebuild", "model_validate", "model_validate_json",
+# "model_validate_strings"
+_RESERVED_NAMES = {key for key in dir(BaseModel) if not key.startswith("_")}
+
+
+def _remap_field_definitions(field_definitions: dict[str, Any]) -> dict[str, Any]:
+ """This remaps fields to avoid colliding with internal pydantic fields."""
+
+ remapped = {}
+ for key, value in field_definitions.items():
+ if key.startswith("_") or key in _RESERVED_NAMES:
+ # Let's add a prefix to avoid colliding with internal pydantic fields
+ if isinstance(value, FieldInfo):
+ msg = (
+ f"Remapping for fields starting with '_' or fields with a name "
+ f"matching a reserved name {_RESERVED_NAMES} is not supported if "
+ f" the field is a pydantic Field instance. Got {key}."
+ )
+ raise NotImplementedError(msg)
+ type_, default_ = value
+ remapped[f"private_{key}"] = (
+ type_,
+ Field(
+ default=default_,
+ alias=key,
+ serialization_alias=key,
+ title=key.lstrip("_").replace("_", " ").title(),
+ ),
+ )
+ else:
+ remapped[key] = value
+ return remapped
+
+
+def create_model(
+ model_name: str,
+ *,
+ field_definitions: dict[str, Any] | None = None,
+ root: Any | None = None,
+) -> type[BaseModel]:
+ """Create a pydantic model with the given field definitions.
+
+ Attention:
+ Please do not use outside of langchain packages. This API
+ is subject to change at any time.
+
+ Args:
+ model_name: The name of the model.
+ module_name: The name of the module where the model is defined.
+ This is used by Pydantic to resolve any forward references.
+ field_definitions: The field definitions for the model.
+ root: Type for a root model (RootModel)
+
+ Returns:
+ Type[BaseModel]: The created model.
+ """
+ field_definitions = field_definitions or {}
+
+ if root:
+ if field_definitions:
+ msg = (
+ "When specifying __root__ no other "
+ f"fields should be provided. Got {field_definitions}"
+ )
+ raise NotImplementedError(msg)
+
+ if isinstance(root, tuple):
+ kwargs = {"type_": root[0], "default_": root[1]}
+ else:
+ kwargs = {"type_": root}
+
+ try:
+ named_root_model = _create_root_model_cached(model_name, **kwargs)
+ except TypeError:
+ # something in the arguments into _create_root_model_cached is not hashable
+ named_root_model = _create_root_model(
+ model_name,
+ **kwargs,
+ )
+ return named_root_model
+
+ # No root, just field definitions
+ names = set(field_definitions.keys())
+
+ capture_warnings = False
+
+ for name in names:
+ # Also if any non-reserved name is used (e.g., model_id or model_name)
+ if name.startswith("model"):
+ capture_warnings = True
+
+ with warnings.catch_warnings() if capture_warnings else nullcontext():
+ if capture_warnings:
+ warnings.filterwarnings(action="ignore")
+ try:
+ return _create_model_cached(model_name, **field_definitions)
+ except TypeError:
+ # something in field definitions is not hashable
+ return _create_model_base(
+ model_name,
+ __config__=_SchemaConfig,
+ **_remap_field_definitions(field_definitions),
+ )
+
+
+def is_supported_by_pydantic(type_: Any) -> bool:
+ """Check if a given "complex" type is supported by pydantic.
+
+ This will return False for primitive types like int, str, etc.
+
+ The check is meant for container types like dataclasses, TypedDicts, etc.
+ """
+ if is_dataclass(type_):
+ return True
+
+ if isinstance(type_, type) and issubclass(type_, BaseModel):
+ return True
+
+ if hasattr(type_, "__orig_bases__"):
+ for base in type_.__orig_bases__:
+ if base is TypedDict:
+ return True
+ elif base is typing.TypedDict: # noqa: TID251
+ # ignoring TID251 since it's OK to use typing.TypedDict in this case.
+ # Pydantic supports typing.TypedDict from Python 3.12
+ # For older versions, only typing_extensions.TypedDict is supported.
+ if sys.version_info >= (3, 12):
+ return True
+ return False
diff --git a/langgraph/source/libs/langgraph/langgraph/_internal/_queue.py b/langgraph/source/libs/langgraph/langgraph/_internal/_queue.py
new file mode 100644
index 0000000000000000000000000000000000000000..a7e48c486b53394c926205ab812cb220c7c296d4
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/_internal/_queue.py
@@ -0,0 +1,124 @@
+# type: ignore
+from __future__ import annotations
+
+import asyncio
+import queue
+import threading
+import types
+from collections import deque
+from time import monotonic
+
+
+class AsyncQueue(asyncio.Queue):
+ """Async unbounded FIFO queue with a wait() method.
+
+ Subclassed from asyncio.Queue, adding a wait() method."""
+
+ async def wait(self) -> None:
+ """If queue is empty, wait until an item is available.
+
+ Copied from Queue.get(), removing the call to .get_nowait(),
+ ie. this doesn't consume the item, just waits for it.
+ """
+ while self.empty():
+ getter = self._get_loop().create_future()
+ self._getters.append(getter)
+ try:
+ await getter
+ except:
+ getter.cancel() # Just in case getter is not done yet.
+ try:
+ # Clean self._getters from canceled getters.
+ self._getters.remove(getter)
+ except ValueError:
+ # The getter could be removed from self._getters by a
+ # previous put_nowait call.
+ pass
+ if not self.empty() and not getter.cancelled():
+ # We were woken up by put_nowait(), but can't take
+ # the call. Wake up the next in line.
+ self._wakeup_next(self._getters)
+ raise
+
+
+class Semaphore(threading.Semaphore):
+ """Semaphore subclass with a wait() method."""
+
+ def wait(self, blocking: bool = True, timeout: float | None = None):
+ """Block until the semaphore can be acquired, but don't acquire it."""
+ if not blocking and timeout is not None:
+ raise ValueError("can't specify timeout for non-blocking acquire")
+ rc = False
+ endtime = None
+ with self._cond:
+ while self._value == 0:
+ if not blocking:
+ break
+ if timeout is not None:
+ if endtime is None:
+ endtime = monotonic() + timeout
+ else:
+ timeout = endtime - monotonic()
+ if timeout <= 0:
+ break
+ self._cond.wait(timeout)
+ else:
+ rc = True
+ return rc
+
+
+class SyncQueue:
+ """Unbounded FIFO queue with a wait() method.
+ Adapted from pure Python implementation of queue.SimpleQueue.
+ """
+
+ def __init__(self):
+ self._queue = deque()
+ self._count = Semaphore(0)
+
+ def put(self, item, block=True, timeout=None):
+ """Put the item on the queue.
+
+ The optional 'block' and 'timeout' arguments are ignored, as this method
+ never blocks. They are provided for compatibility with the Queue class.
+ """
+ self._queue.append(item)
+ self._count.release()
+
+ def get(self, block=False, timeout=None):
+ """Remove and return an item from the queue.
+
+ If optional args 'block' is true and 'timeout' is None (the default),
+ block if necessary until an item is available. If 'timeout' is
+ a non-negative number, it blocks at most 'timeout' seconds and raises
+ the Empty exception if no item was available within that time.
+ Otherwise ('block' is false), return an item if one is immediately
+ available, else raise the Empty exception ('timeout' is ignored
+ in that case).
+ """
+ if timeout is not None and timeout < 0:
+ raise ValueError("'timeout' must be a non-negative number")
+ if not self._count.acquire(block, timeout):
+ raise queue.Empty
+ try:
+ return self._queue.popleft()
+ except IndexError:
+ raise queue.Empty
+
+ def wait(self, block=True, timeout=None):
+ """If queue is empty, wait until an item maybe is available,
+ but don't consume it.
+ """
+ if timeout is not None and timeout < 0:
+ raise ValueError("'timeout' must be a non-negative number")
+ self._count.wait(block, timeout)
+
+ def empty(self):
+ """Return True if the queue is empty, False otherwise (not reliable!)."""
+ return len(self._queue) == 0
+
+ def qsize(self):
+ """Return the approximate size of the queue (not reliable!)."""
+ return len(self._queue)
+
+ __class_getitem__ = classmethod(types.GenericAlias)
diff --git a/langgraph/source/libs/langgraph/langgraph/_internal/_retry.py b/langgraph/source/libs/langgraph/langgraph/_internal/_retry.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d4e41fd78fc046530246c05555864bcc50ea44a
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/_internal/_retry.py
@@ -0,0 +1,29 @@
+def default_retry_on(exc: Exception) -> bool:
+ import httpx
+ import requests
+
+ if isinstance(exc, ConnectionError):
+ return True
+ if isinstance(exc, httpx.HTTPStatusError):
+ return 500 <= exc.response.status_code < 600
+ if isinstance(exc, requests.HTTPError):
+ return 500 <= exc.response.status_code < 600 if exc.response else True
+ if isinstance(
+ exc,
+ (
+ ValueError,
+ TypeError,
+ ArithmeticError,
+ ImportError,
+ LookupError,
+ NameError,
+ SyntaxError,
+ RuntimeError,
+ ReferenceError,
+ StopIteration,
+ StopAsyncIteration,
+ OSError,
+ ),
+ ):
+ return False
+ return True
diff --git a/langgraph/source/libs/langgraph/langgraph/_internal/_runnable.py b/langgraph/source/libs/langgraph/langgraph/_internal/_runnable.py
new file mode 100644
index 0000000000000000000000000000000000000000..63e03f544ac94c5c0adeedb94e2574ca7286859a
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/_internal/_runnable.py
@@ -0,0 +1,914 @@
+from __future__ import annotations
+
+import asyncio
+import enum
+import inspect
+import sys
+import warnings
+from collections.abc import (
+ AsyncIterator,
+ Awaitable,
+ Callable,
+ Coroutine,
+ Generator,
+ Iterator,
+ Sequence,
+)
+from contextlib import AsyncExitStack, contextmanager
+from contextvars import Context, Token, copy_context
+from functools import partial, wraps
+from typing import (
+ Any,
+ Optional,
+ Protocol,
+ TypeGuard,
+ cast,
+)
+
+from langchain_core.runnables.base import (
+ Runnable,
+ RunnableConfig,
+ RunnableLambda,
+ RunnableParallel,
+ RunnableSequence,
+)
+from langchain_core.runnables.base import (
+ RunnableLike as LCRunnableLike,
+)
+from langchain_core.runnables.config import (
+ run_in_executor,
+ var_child_runnable_config,
+)
+from langchain_core.runnables.utils import Input, Output
+from langchain_core.tracers.langchain import LangChainTracer
+from langgraph.store.base import BaseStore
+
+from langgraph._internal._config import (
+ ensure_config,
+ get_async_callback_manager_for_config,
+ get_callback_manager_for_config,
+ patch_config,
+)
+from langgraph._internal._constants import (
+ CONF,
+ CONFIG_KEY_RUNTIME,
+)
+from langgraph._internal._typing import MISSING
+from langgraph.types import StreamWriter
+
+try:
+ from langchain_core.tracers._streaming import _StreamingCallbackHandler
+except ImportError:
+ _StreamingCallbackHandler = None # type: ignore
+
+
+def _set_config_context(
+ config: RunnableConfig, run: Any = None
+) -> Token[RunnableConfig | None]:
+ """Set the child Runnable config + tracing context.
+
+ Args:
+ config: The config to set.
+ """
+ config_token = var_child_runnable_config.set(config)
+ if run is not None:
+ from langsmith.run_helpers import _set_tracing_context
+
+ _set_tracing_context({"parent": run})
+ return config_token
+
+
+def _unset_config_context(token: Token[RunnableConfig | None], run: Any = None) -> None:
+ """Set the child Runnable config + tracing context.
+
+ Args:
+ token: The config token to reset.
+ """
+ var_child_runnable_config.reset(token)
+ if run is not None:
+ from langsmith.run_helpers import _set_tracing_context
+
+ _set_tracing_context(
+ {
+ "parent": None,
+ "project_name": None,
+ "tags": None,
+ "metadata": None,
+ "enabled": None,
+ "client": None,
+ }
+ )
+
+
+@contextmanager
+def set_config_context(
+ config: RunnableConfig, run: Any = None
+) -> Generator[Context, None, None]:
+ """Set the child Runnable config + tracing context.
+
+ Args:
+ config: The config to set.
+ """
+ ctx = copy_context()
+ config_token = ctx.run(_set_config_context, config, run)
+ try:
+ yield ctx
+ finally:
+ ctx.run(_unset_config_context, config_token, run)
+
+
+# Before Python 3.11 native StrEnum is not available
+class StrEnum(str, enum.Enum):
+ """A string enum."""
+
+
+# Special type to denote any type is accepted
+ANY_TYPE = object()
+
+ASYNCIO_ACCEPTS_CONTEXT = sys.version_info >= (3, 11)
+
+# List of keyword arguments that can be injected into nodes / tasks / tools at runtime.
+# A named argument may appear multiple times if it appears with distinct types.
+KWARGS_CONFIG_KEYS: tuple[tuple[str, tuple[Any, ...], str, Any], ...] = (
+ (
+ "config",
+ (
+ RunnableConfig,
+ "RunnableConfig",
+ Optional[RunnableConfig], # noqa: UP045
+ "Optional[RunnableConfig]",
+ inspect.Parameter.empty,
+ ),
+ # for now, use config directly, eventually, will pop off of Runtime
+ "N/A",
+ inspect.Parameter.empty,
+ ),
+ (
+ "writer",
+ (StreamWriter, "StreamWriter", inspect.Parameter.empty),
+ "stream_writer",
+ lambda _: None,
+ ),
+ (
+ "store",
+ (
+ BaseStore,
+ "BaseStore",
+ inspect.Parameter.empty,
+ ),
+ "store",
+ inspect.Parameter.empty,
+ ),
+ (
+ "store",
+ (
+ Optional[BaseStore], # noqa: UP045
+ "Optional[BaseStore]",
+ ),
+ "store",
+ None,
+ ),
+ (
+ "previous",
+ (ANY_TYPE,),
+ "previous",
+ inspect.Parameter.empty,
+ ),
+ (
+ "runtime",
+ (ANY_TYPE,),
+ # we never hit this block, we just inject runtime directly
+ "N/A",
+ inspect.Parameter.empty,
+ ),
+)
+"""List of kwargs that can be passed to functions, and their corresponding
+config keys, default values and type annotations.
+
+Used to configure keyword arguments that can be injected at runtime
+from the `Runtime` object as kwargs to `invoke`, `ainvoke`, `stream` and `astream`.
+
+For a keyword to be injected from the config object, the function signature
+must contain a kwarg with the same name and a matching type annotation.
+
+Each tuple contains:
+- the name of the kwarg in the function signature
+- the type annotation(s) for the kwarg
+- the `Runtime` attribute for fetching the value (N/A if not applicable)
+
+This is fully internal and should be further refactored to use `get_type_hints`
+to resolve forward references and optional types formatted like BaseStore | None.
+"""
+
+VALID_KINDS = (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
+
+
+class _RunnableWithWriter(Protocol[Input, Output]):
+ def __call__(self, state: Input, *, writer: StreamWriter) -> Output: ...
+
+
+class _RunnableWithStore(Protocol[Input, Output]):
+ def __call__(self, state: Input, *, store: BaseStore) -> Output: ...
+
+
+class _RunnableWithWriterStore(Protocol[Input, Output]):
+ def __call__(
+ self, state: Input, *, writer: StreamWriter, store: BaseStore
+ ) -> Output: ...
+
+
+class _RunnableWithConfigWriter(Protocol[Input, Output]):
+ def __call__(
+ self, state: Input, *, config: RunnableConfig, writer: StreamWriter
+ ) -> Output: ...
+
+
+class _RunnableWithConfigStore(Protocol[Input, Output]):
+ def __call__(
+ self, state: Input, *, config: RunnableConfig, store: BaseStore
+ ) -> Output: ...
+
+
+class _RunnableWithConfigWriterStore(Protocol[Input, Output]):
+ def __call__(
+ self,
+ state: Input,
+ *,
+ config: RunnableConfig,
+ writer: StreamWriter,
+ store: BaseStore,
+ ) -> Output: ...
+
+
+RunnableLike = (
+ LCRunnableLike
+ | _RunnableWithWriter[Input, Output]
+ | _RunnableWithStore[Input, Output]
+ | _RunnableWithWriterStore[Input, Output]
+ | _RunnableWithConfigWriter[Input, Output]
+ | _RunnableWithConfigStore[Input, Output]
+ | _RunnableWithConfigWriterStore[Input, Output]
+)
+
+
+class RunnableCallable(Runnable):
+ """A much simpler version of RunnableLambda that requires sync and async functions."""
+
+ def __init__(
+ self,
+ func: Callable[..., Any | Runnable] | None,
+ afunc: Callable[..., Awaitable[Any | Runnable]] | None = None,
+ *,
+ name: str | None = None,
+ tags: Sequence[str] | None = None,
+ trace: bool = True,
+ recurse: bool = True,
+ explode_args: bool = False,
+ **kwargs: Any,
+ ) -> None:
+ self.name = name
+ if self.name is None:
+ if func:
+ try:
+ if func.__name__ != "":
+ self.name = func.__name__
+ except AttributeError:
+ pass
+ elif afunc:
+ try:
+ self.name = afunc.__name__
+ except AttributeError:
+ pass
+ self.func = func
+ self.afunc = afunc
+ self.tags = tags
+ self.kwargs = kwargs
+ self.trace = trace
+ self.recurse = recurse
+ self.explode_args = explode_args
+ # check signature
+ if func is None and afunc is None:
+ raise ValueError("At least one of func or afunc must be provided.")
+
+ self.func_accepts: dict[str, tuple[str, Any]] = {}
+ params = inspect.signature(cast(Callable, func or afunc)).parameters
+
+ for kw, typ, runtime_key, default in KWARGS_CONFIG_KEYS:
+ p = params.get(kw)
+
+ if p is None or p.kind not in VALID_KINDS:
+ # If parameter is not found or is not a valid kind, skip
+ continue
+
+ if typ != (ANY_TYPE,) and p.annotation not in typ:
+ # A specific type is required, but the function annotation does
+ # not match the expected type.
+
+ # If this is a config parameter with incorrect typing, emit a warning
+ # because we used to support any type but are moving towards more correct typing
+ if kw == "config" and p.annotation != inspect.Parameter.empty:
+ warnings.warn(
+ f"The 'config' parameter should be typed as 'RunnableConfig' or "
+ f"'RunnableConfig | None', not '{p.annotation}'. ",
+ UserWarning,
+ stacklevel=4,
+ )
+ continue
+
+ # If the kwarg is accepted by the function, store the key / runtime attribute to inject
+ self.func_accepts[kw] = (runtime_key, default)
+
+ def __repr__(self) -> str:
+ repr_args = {
+ k: v
+ for k, v in self.__dict__.items()
+ if k not in {"name", "func", "afunc", "config", "kwargs", "trace"}
+ }
+ return f"{self.get_name()}({', '.join(f'{k}={v!r}' for k, v in repr_args.items())})"
+
+ def invoke(
+ self, input: Any, config: RunnableConfig | None = None, **kwargs: Any
+ ) -> Any:
+ if self.func is None:
+ raise TypeError(
+ f'No synchronous function provided to "{self.name}".'
+ "\nEither initialize with a synchronous function or invoke"
+ " via the async API (ainvoke, astream, etc.)"
+ )
+ if config is None:
+ config = ensure_config()
+ if self.explode_args:
+ args, _kwargs = input
+ kwargs = {**self.kwargs, **_kwargs, **kwargs}
+ else:
+ args = (input,)
+ kwargs = {**self.kwargs, **kwargs}
+
+ runtime = config.get(CONF, {}).get(CONFIG_KEY_RUNTIME)
+
+ for kw, (runtime_key, default) in self.func_accepts.items():
+ # If the kwarg is already set, use the set value
+ if kw in kwargs:
+ continue
+
+ kw_value: Any = MISSING
+ if kw == "config":
+ kw_value = config
+ elif runtime:
+ if kw == "runtime":
+ kw_value = runtime
+ else:
+ try:
+ kw_value = getattr(runtime, runtime_key)
+ except AttributeError:
+ pass
+
+ if kw_value is MISSING:
+ if default is inspect.Parameter.empty:
+ raise ValueError(
+ f"Missing required config key '{runtime_key}' for '{self.name}'."
+ )
+ kw_value = default
+ kwargs[kw] = kw_value
+
+ if self.trace:
+ callback_manager = get_callback_manager_for_config(config, self.tags)
+ run_manager = callback_manager.on_chain_start(
+ None,
+ input,
+ name=config.get("run_name") or self.get_name(),
+ run_id=config.pop("run_id", None),
+ )
+ try:
+ child_config = patch_config(config, callbacks=run_manager.get_child())
+ # get the run
+ for h in run_manager.handlers:
+ if isinstance(h, LangChainTracer):
+ run = h.run_map.get(str(run_manager.run_id))
+ break
+ else:
+ run = None
+ # run in context
+ with set_config_context(child_config, run) as context:
+ ret = context.run(self.func, *args, **kwargs)
+ except BaseException as e:
+ run_manager.on_chain_error(e)
+ raise
+ else:
+ run_manager.on_chain_end(ret)
+ else:
+ ret = self.func(*args, **kwargs)
+ if self.recurse and isinstance(ret, Runnable):
+ return ret.invoke(input, config)
+ return ret
+
+ async def ainvoke(
+ self, input: Any, config: RunnableConfig | None = None, **kwargs: Any
+ ) -> Any:
+ if not self.afunc:
+ return self.invoke(input, config)
+ if config is None:
+ config = ensure_config()
+ if self.explode_args:
+ args, _kwargs = input
+ kwargs = {**self.kwargs, **_kwargs, **kwargs}
+ else:
+ args = (input,)
+ kwargs = {**self.kwargs, **kwargs}
+
+ runtime = config.get(CONF, {}).get(CONFIG_KEY_RUNTIME)
+
+ for kw, (runtime_key, default) in self.func_accepts.items():
+ # If the kwarg has already been set, use the set value
+ if kw in kwargs:
+ continue
+
+ kw_value: Any = MISSING
+ if kw == "config":
+ kw_value = config
+ elif runtime:
+ if kw == "runtime":
+ kw_value = runtime
+ else:
+ try:
+ kw_value = getattr(runtime, runtime_key)
+ except AttributeError:
+ pass
+ if kw_value is MISSING:
+ if default is inspect.Parameter.empty:
+ raise ValueError(
+ f"Missing required config key '{runtime_key}' for '{self.name}'."
+ )
+ kw_value = default
+ kwargs[kw] = kw_value
+
+ if self.trace:
+ callback_manager = get_async_callback_manager_for_config(config, self.tags)
+ run_manager = await callback_manager.on_chain_start(
+ None,
+ input,
+ name=config.get("run_name") or self.name,
+ run_id=config.pop("run_id", None),
+ )
+ try:
+ child_config = patch_config(config, callbacks=run_manager.get_child())
+ coro = cast(Coroutine[None, None, Any], self.afunc(*args, **kwargs))
+ if ASYNCIO_ACCEPTS_CONTEXT:
+ for h in run_manager.handlers:
+ if isinstance(h, LangChainTracer):
+ run = h.run_map.get(str(run_manager.run_id))
+ break
+ else:
+ run = None
+ with set_config_context(child_config, run) as context:
+ ret = await asyncio.create_task(coro, context=context)
+ else:
+ ret = await coro
+ except BaseException as e:
+ await run_manager.on_chain_error(e)
+ raise
+ else:
+ await run_manager.on_chain_end(ret)
+ else:
+ ret = await self.afunc(*args, **kwargs)
+ if self.recurse and isinstance(ret, Runnable):
+ return await ret.ainvoke(input, config)
+ return ret
+
+
+def is_async_callable(
+ func: Any,
+) -> TypeGuard[Callable[..., Awaitable]]:
+ """Check if a function is async."""
+ return (
+ inspect.iscoroutinefunction(func)
+ or hasattr(func, "__call__")
+ and inspect.iscoroutinefunction(func.__call__)
+ )
+
+
+def is_async_generator(
+ func: Any,
+) -> TypeGuard[Callable[..., AsyncIterator]]:
+ """Check if a function is an async generator."""
+ return (
+ inspect.isasyncgenfunction(func)
+ or hasattr(func, "__call__")
+ and inspect.isasyncgenfunction(func.__call__)
+ )
+
+
+def coerce_to_runnable(
+ thing: RunnableLike, *, name: str | None, trace: bool
+) -> Runnable:
+ """Coerce a runnable-like object into a Runnable.
+
+ Args:
+ thing: A runnable-like object.
+
+ Returns:
+ A Runnable.
+ """
+ if isinstance(thing, Runnable):
+ return thing
+ elif is_async_generator(thing) or inspect.isgeneratorfunction(thing):
+ return RunnableLambda(thing, name=name)
+ elif callable(thing):
+ if is_async_callable(thing):
+ return RunnableCallable(None, thing, name=name, trace=trace)
+ else:
+ return RunnableCallable(
+ thing,
+ wraps(thing)(partial(run_in_executor, None, thing)), # type: ignore[arg-type]
+ name=name,
+ trace=trace,
+ )
+ elif isinstance(thing, dict):
+ return RunnableParallel(thing)
+ else:
+ raise TypeError(
+ f"Expected a Runnable, callable or dict."
+ f"Instead got an unsupported type: {type(thing)}"
+ )
+
+
+class RunnableSeq(Runnable):
+ """Sequence of `Runnable`, where the output of each is the input of the next.
+
+ `RunnableSeq` is a simpler version of `RunnableSequence` that is internal to
+ LangGraph.
+ """
+
+ def __init__(
+ self,
+ *steps: RunnableLike,
+ name: str | None = None,
+ trace_inputs: Callable[[Any], Any] | None = None,
+ ) -> None:
+ """Create a new RunnableSeq.
+
+ Args:
+ steps: The steps to include in the sequence.
+ name: The name of the `Runnable`.
+
+ Raises:
+ ValueError: If the sequence has less than 2 steps.
+ """
+ steps_flat: list[Runnable] = []
+ for step in steps:
+ if isinstance(step, RunnableSequence):
+ steps_flat.extend(step.steps)
+ elif isinstance(step, RunnableSeq):
+ steps_flat.extend(step.steps)
+ else:
+ steps_flat.append(coerce_to_runnable(step, name=None, trace=True))
+ if len(steps_flat) < 2:
+ raise ValueError(
+ f"RunnableSeq must have at least 2 steps, got {len(steps_flat)}"
+ )
+ self.steps = steps_flat
+ self.name = name
+ self.trace_inputs = trace_inputs
+
+ def __or__(
+ self,
+ other: Any,
+ ) -> Runnable:
+ if isinstance(other, RunnableSequence):
+ return RunnableSeq(
+ *self.steps,
+ other.first,
+ *other.middle,
+ other.last,
+ name=self.name or other.name,
+ )
+ elif isinstance(other, RunnableSeq):
+ return RunnableSeq(
+ *self.steps,
+ *other.steps,
+ name=self.name or other.name,
+ )
+ else:
+ return RunnableSeq(
+ *self.steps,
+ coerce_to_runnable(other, name=None, trace=True),
+ name=self.name,
+ )
+
+ def __ror__(
+ self,
+ other: Any,
+ ) -> Runnable:
+ if isinstance(other, RunnableSequence):
+ return RunnableSequence(
+ other.first,
+ *other.middle,
+ other.last,
+ *self.steps,
+ name=other.name or self.name,
+ )
+ elif isinstance(other, RunnableSeq):
+ return RunnableSeq(
+ *other.steps,
+ *self.steps,
+ name=other.name or self.name,
+ )
+ else:
+ return RunnableSequence(
+ coerce_to_runnable(other, name=None, trace=True),
+ *self.steps,
+ name=self.name,
+ )
+
+ def invoke(
+ self, input: Input, config: RunnableConfig | None = None, **kwargs: Any
+ ) -> Any:
+ if config is None:
+ config = ensure_config()
+ # setup callbacks and context
+ callback_manager = get_callback_manager_for_config(config)
+ # start the root run
+ run_manager = callback_manager.on_chain_start(
+ None,
+ self.trace_inputs(input) if self.trace_inputs is not None else input,
+ name=config.get("run_name") or self.get_name(),
+ run_id=config.pop("run_id", None),
+ )
+ # invoke all steps in sequence
+ try:
+ for i, step in enumerate(self.steps):
+ # mark each step as a child run
+ config = patch_config(
+ config, callbacks=run_manager.get_child(f"seq:step:{i + 1}")
+ )
+ # 1st step is the actual node,
+ # others are writers which don't need to be run in context
+ if i == 0:
+ # get the run object
+ for h in run_manager.handlers:
+ if isinstance(h, LangChainTracer):
+ run = h.run_map.get(str(run_manager.run_id))
+ break
+ else:
+ run = None
+ # run in context
+ with set_config_context(config, run) as context:
+ input = context.run(step.invoke, input, config, **kwargs)
+ else:
+ input = step.invoke(input, config)
+ # finish the root run
+ except BaseException as e:
+ run_manager.on_chain_error(e)
+ raise
+ else:
+ run_manager.on_chain_end(input)
+ return input
+
+ async def ainvoke(
+ self,
+ input: Input,
+ config: RunnableConfig | None = None,
+ **kwargs: Any | None,
+ ) -> Any:
+ if config is None:
+ config = ensure_config()
+ # setup callbacks
+ callback_manager = get_async_callback_manager_for_config(config)
+ # start the root run
+ run_manager = await callback_manager.on_chain_start(
+ None,
+ self.trace_inputs(input) if self.trace_inputs is not None else input,
+ name=config.get("run_name") or self.get_name(),
+ run_id=config.pop("run_id", None),
+ )
+
+ # invoke all steps in sequence
+ try:
+ for i, step in enumerate(self.steps):
+ # mark each step as a child run
+ config = patch_config(
+ config, callbacks=run_manager.get_child(f"seq:step:{i + 1}")
+ )
+ # 1st step is the actual node,
+ # others are writers which don't need to be run in context
+ if i == 0:
+ if ASYNCIO_ACCEPTS_CONTEXT:
+ # get the run object
+ for h in run_manager.handlers:
+ if isinstance(h, LangChainTracer):
+ run = h.run_map.get(str(run_manager.run_id))
+ break
+ else:
+ run = None
+ # run in context
+ with set_config_context(config, run) as context:
+ input = await asyncio.create_task(
+ step.ainvoke(input, config, **kwargs), context=context
+ )
+ else:
+ input = await step.ainvoke(input, config, **kwargs)
+ else:
+ input = await step.ainvoke(input, config)
+ # finish the root run
+ except BaseException as e:
+ await run_manager.on_chain_error(e)
+ raise
+ else:
+ await run_manager.on_chain_end(input)
+ return input
+
+ def stream(
+ self,
+ input: Input,
+ config: RunnableConfig | None = None,
+ **kwargs: Any | None,
+ ) -> Iterator[Any]:
+ if config is None:
+ config = ensure_config()
+ # setup callbacks
+ callback_manager = get_callback_manager_for_config(config)
+ # start the root run
+ run_manager = callback_manager.on_chain_start(
+ None,
+ self.trace_inputs(input) if self.trace_inputs is not None else input,
+ name=config.get("run_name") or self.get_name(),
+ run_id=config.pop("run_id", None),
+ )
+ # get the run object
+ for h in run_manager.handlers:
+ if isinstance(h, LangChainTracer):
+ run = h.run_map.get(str(run_manager.run_id))
+ break
+ else:
+ run = None
+ # create first step config
+ config = patch_config(
+ config,
+ callbacks=run_manager.get_child(f"seq:step:{1}"),
+ )
+ # run all in context
+ with set_config_context(config, run) as context:
+ try:
+ # stream the last steps
+ # transform the input stream of each step with the next
+ # steps that don't natively support transforming an input stream will
+ # buffer input in memory until all available, and then start emitting output
+ for idx, step in enumerate(self.steps):
+ if idx == 0:
+ iterator = step.stream(input, config, **kwargs)
+ else:
+ config = patch_config(
+ config,
+ callbacks=run_manager.get_child(f"seq:step:{idx + 1}"),
+ )
+ iterator = step.transform(iterator, config)
+ # populates streamed_output in astream_log() output if needed
+ if _StreamingCallbackHandler is not None:
+ for h in run_manager.handlers:
+ if isinstance(h, _StreamingCallbackHandler):
+ iterator = h.tap_output_iter(run_manager.run_id, iterator)
+ # consume into final output
+ output = context.run(_consume_iter, iterator)
+ # sequence doesn't emit output, yield to mark as generator
+ yield
+ except BaseException as e:
+ run_manager.on_chain_error(e)
+ raise
+ else:
+ run_manager.on_chain_end(output)
+
+ async def astream(
+ self,
+ input: Input,
+ config: RunnableConfig | None = None,
+ **kwargs: Any | None,
+ ) -> AsyncIterator[Any]:
+ if config is None:
+ config = ensure_config()
+ # setup callbacks
+ callback_manager = get_async_callback_manager_for_config(config)
+ # start the root run
+ run_manager = await callback_manager.on_chain_start(
+ None,
+ self.trace_inputs(input) if self.trace_inputs is not None else input,
+ name=config.get("run_name") or self.get_name(),
+ run_id=config.pop("run_id", None),
+ )
+ # stream the last steps
+ # transform the input stream of each step with the next
+ # steps that don't natively support transforming an input stream will
+ # buffer input in memory until all available, and then start emitting output
+ if ASYNCIO_ACCEPTS_CONTEXT:
+ # get the run object
+ for h in run_manager.handlers:
+ if isinstance(h, LangChainTracer):
+ run = h.run_map.get(str(run_manager.run_id))
+ break
+ else:
+ run = None
+ # create first step config
+ config = patch_config(
+ config,
+ callbacks=run_manager.get_child(f"seq:step:{1}"),
+ )
+ # run all in context
+ with set_config_context(config, run) as context:
+ try:
+ async with AsyncExitStack() as stack:
+ for idx, step in enumerate(self.steps):
+ if idx == 0:
+ aiterator = step.astream(input, config, **kwargs)
+ else:
+ config = patch_config(
+ config,
+ callbacks=run_manager.get_child(
+ f"seq:step:{idx + 1}"
+ ),
+ )
+ aiterator = step.atransform(aiterator, config)
+ if hasattr(aiterator, "aclose"):
+ stack.push_async_callback(aiterator.aclose)
+ # populates streamed_output in astream_log() output if needed
+ if _StreamingCallbackHandler is not None:
+ for h in run_manager.handlers:
+ if isinstance(h, _StreamingCallbackHandler):
+ aiterator = h.tap_output_aiter(
+ run_manager.run_id, aiterator
+ )
+ # consume into final output
+ output = await asyncio.create_task(
+ _consume_aiter(aiterator), context=context
+ )
+ # sequence doesn't emit output, yield to mark as generator
+ yield
+ except BaseException as e:
+ await run_manager.on_chain_error(e)
+ raise
+ else:
+ await run_manager.on_chain_end(output)
+ else:
+ try:
+ async with AsyncExitStack() as stack:
+ for idx, step in enumerate(self.steps):
+ config = patch_config(
+ config,
+ callbacks=run_manager.get_child(f"seq:step:{idx + 1}"),
+ )
+ if idx == 0:
+ aiterator = step.astream(input, config, **kwargs)
+ else:
+ aiterator = step.atransform(aiterator, config)
+ if hasattr(aiterator, "aclose"):
+ stack.push_async_callback(aiterator.aclose)
+ # populates streamed_output in astream_log() output if needed
+ if _StreamingCallbackHandler is not None:
+ for h in run_manager.handlers:
+ if isinstance(h, _StreamingCallbackHandler):
+ aiterator = h.tap_output_aiter(
+ run_manager.run_id, aiterator
+ )
+ # consume into final output
+ output = await _consume_aiter(aiterator)
+ # sequence doesn't emit output, yield to mark as generator
+ yield
+ except BaseException as e:
+ await run_manager.on_chain_error(e)
+ raise
+ else:
+ await run_manager.on_chain_end(output)
+
+
+def _consume_iter(it: Iterator[Any]) -> Any:
+ """Consume an iterator."""
+ output: Any = None
+ add_supported = False
+ for chunk in it:
+ # collect final output
+ if output is None:
+ output = chunk
+ elif add_supported:
+ try:
+ output = output + chunk
+ except TypeError:
+ output = chunk
+ add_supported = False
+ else:
+ output = chunk
+ return output
+
+
+async def _consume_aiter(it: AsyncIterator[Any]) -> Any:
+ """Consume an async iterator."""
+ output: Any = None
+ add_supported = False
+ async for chunk in it:
+ # collect final output
+ if add_supported:
+ try:
+ output = output + chunk
+ except TypeError:
+ output = chunk
+ add_supported = False
+ else:
+ output = chunk
+ return output
diff --git a/langgraph/source/libs/langgraph/langgraph/_internal/_scratchpad.py b/langgraph/source/libs/langgraph/langgraph/_internal/_scratchpad.py
new file mode 100644
index 0000000000000000000000000000000000000000..fd96b726c66276761acc88d88324d9bc3d7ea7d1
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/_internal/_scratchpad.py
@@ -0,0 +1,19 @@
+import dataclasses
+from collections.abc import Callable
+from typing import Any
+
+from langgraph.types import _DC_KWARGS
+
+
+@dataclasses.dataclass(**_DC_KWARGS)
+class PregelScratchpad:
+ step: int
+ stop: int
+ # call
+ call_counter: Callable[[], int]
+ # interrupt
+ interrupt_counter: Callable[[], int]
+ get_null_resume: Callable[[bool], Any]
+ resume: list[Any]
+ # subgraph
+ subgraph_counter: Callable[[], int]
diff --git a/langgraph/source/libs/langgraph/langgraph/_internal/_typing.py b/langgraph/source/libs/langgraph/langgraph/_internal/_typing.py
new file mode 100644
index 0000000000000000000000000000000000000000..19854d1c814ba83a9236aef6816645ccc17e2e85
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/_internal/_typing.py
@@ -0,0 +1,54 @@
+"""Private typing utilities for LangGraph."""
+
+from __future__ import annotations
+
+from dataclasses import Field
+from typing import Any, ClassVar, Protocol, TypeAlias
+
+from pydantic import BaseModel
+from typing_extensions import TypedDict
+
+
+class TypedDictLikeV1(Protocol):
+ """Protocol to represent types that behave like TypedDicts
+
+ Version 1: using `ClassVar` for keys."""
+
+ __required_keys__: ClassVar[frozenset[str]]
+ __optional_keys__: ClassVar[frozenset[str]]
+
+
+class TypedDictLikeV2(Protocol):
+ """Protocol to represent types that behave like TypedDicts
+
+ Version 2: not using `ClassVar` for keys."""
+
+ __required_keys__: frozenset[str]
+ __optional_keys__: frozenset[str]
+
+
+class DataclassLike(Protocol):
+ """Protocol to represent types that behave like dataclasses.
+
+ Inspired by the private _DataclassT from dataclasses that uses a similar protocol as a bound."""
+
+ __dataclass_fields__: ClassVar[dict[str, Field[Any]]]
+
+
+StateLike: TypeAlias = TypedDictLikeV1 | TypedDictLikeV2 | DataclassLike | BaseModel
+"""Type alias for state-like types.
+
+It can either be a `TypedDict`, `dataclass`, or Pydantic `BaseModel`.
+Note: we cannot use either `TypedDict` or `dataclass` directly due to limitations in type checking.
+"""
+
+MISSING = object()
+"""Unset sentinel value."""
+
+
+class DeprecatedKwargs(TypedDict):
+ """TypedDict to use for extra keyword arguments, enabling type checking warnings for deprecated arguments."""
+
+
+EMPTY_SEQ: tuple[str, ...] = tuple()
+"""An empty sequence of strings."""
diff --git a/langgraph/source/libs/langgraph/langgraph/channels/__init__.py b/langgraph/source/libs/langgraph/langgraph/channels/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a69c230b505c1ef8823232ccfc79ab3b607f6d14
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/channels/__init__.py
@@ -0,0 +1,27 @@
+from langgraph.channels.any_value import AnyValue
+from langgraph.channels.base import BaseChannel
+from langgraph.channels.binop import BinaryOperatorAggregate
+from langgraph.channels.ephemeral_value import EphemeralValue
+from langgraph.channels.last_value import LastValue, LastValueAfterFinish
+from langgraph.channels.named_barrier_value import (
+ NamedBarrierValue,
+ NamedBarrierValueAfterFinish,
+)
+from langgraph.channels.topic import Topic
+from langgraph.channels.untracked_value import UntrackedValue
+
+__all__ = (
+ # base
+ "BaseChannel",
+ # value types
+ "AnyValue",
+ "LastValue",
+ "LastValueAfterFinish",
+ "UntrackedValue",
+ "EphemeralValue",
+ "BinaryOperatorAggregate",
+ "NamedBarrierValue",
+ "NamedBarrierValueAfterFinish",
+ # topics
+ "Topic",
+)
diff --git a/langgraph/source/libs/langgraph/langgraph/channels/any_value.py b/langgraph/source/libs/langgraph/langgraph/channels/any_value.py
new file mode 100644
index 0000000000000000000000000000000000000000..9ba25557469dce738422287050393c98714be0fc
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/channels/any_value.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+from collections.abc import Sequence
+from typing import Any, Generic
+
+from typing_extensions import Self
+
+from langgraph._internal._typing import MISSING
+from langgraph.channels.base import BaseChannel, Value
+from langgraph.errors import EmptyChannelError
+
+__all__ = ("AnyValue",)
+
+
+class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]):
+ """Stores the last value received, assumes that if multiple values are
+ received, they are all equal."""
+
+ __slots__ = ("typ", "value")
+
+ value: Value | Any
+
+ def __init__(self, typ: Any, key: str = "") -> None:
+ super().__init__(typ, key)
+ self.value = MISSING
+
+ def __eq__(self, value: object) -> bool:
+ return isinstance(value, AnyValue)
+
+ @property
+ def ValueType(self) -> type[Value]:
+ """The type of the value stored in the channel."""
+ return self.typ
+
+ @property
+ def UpdateType(self) -> type[Value]:
+ """The type of the update received by the channel."""
+ return self.typ
+
+ def copy(self) -> Self:
+ """Return a copy of the channel."""
+ empty = self.__class__(self.typ, self.key)
+ empty.value = self.value
+ return empty
+
+ def from_checkpoint(self, checkpoint: Value) -> Self:
+ empty = self.__class__(self.typ, self.key)
+ if checkpoint is not MISSING:
+ empty.value = checkpoint
+ return empty
+
+ def update(self, values: Sequence[Value]) -> bool:
+ if len(values) == 0:
+ if self.value is MISSING:
+ return False
+ else:
+ self.value = MISSING
+ return True
+
+ self.value = values[-1]
+ return True
+
+ def get(self) -> Value:
+ if self.value is MISSING:
+ raise EmptyChannelError()
+ return self.value
+
+ def is_available(self) -> bool:
+ return self.value is not MISSING
+
+ def checkpoint(self) -> Value:
+ return self.value
diff --git a/langgraph/source/libs/langgraph/langgraph/channels/base.py b/langgraph/source/libs/langgraph/langgraph/channels/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..9207aa2bbf2c1dd3c4a3a32624f882ce845433d5
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/channels/base.py
@@ -0,0 +1,121 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from collections.abc import Sequence
+from typing import Any, Generic, TypeVar
+
+from typing_extensions import Self
+
+from langgraph._internal._typing import MISSING
+from langgraph.errors import EmptyChannelError
+
+Value = TypeVar("Value")
+Update = TypeVar("Update")
+Checkpoint = TypeVar("Checkpoint")
+
+__all__ = ("BaseChannel",)
+
+
+class BaseChannel(Generic[Value, Update, Checkpoint], ABC):
+ """Base class for all channels."""
+
+ __slots__ = ("key", "typ")
+
+ def __init__(self, typ: Any, key: str = "") -> None:
+ self.typ = typ
+ self.key = key
+
+ @property
+ @abstractmethod
+ def ValueType(self) -> Any:
+ """The type of the value stored in the channel."""
+
+ @property
+ @abstractmethod
+ def UpdateType(self) -> Any:
+ """The type of the update received by the channel."""
+
+ # serialize/deserialize methods
+
+ def copy(self) -> Self:
+ """Return a copy of the channel.
+
+ By default, delegates to `checkpoint()` and `from_checkpoint()`.
+
+ Subclasses can override this method with a more efficient implementation.
+ """
+ return self.from_checkpoint(self.checkpoint())
+
+ def checkpoint(self) -> Checkpoint | Any:
+ """Return a serializable representation of the channel's current state.
+
+ Raises `EmptyChannelError` if the channel is empty (never updated yet),
+ or doesn't support checkpoints.
+ """
+ try:
+ return self.get()
+ except EmptyChannelError:
+ return MISSING
+
+ @abstractmethod
+ def from_checkpoint(self, checkpoint: Checkpoint | Any) -> Self:
+ """Return a new identical channel, optionally initialized from a checkpoint.
+
+ If the checkpoint contains complex data structures, they should be copied.
+ """
+
+ # read methods
+
+ @abstractmethod
+ def get(self) -> Value:
+ """Return the current value of the channel.
+
+ Raises `EmptyChannelError` if the channel is empty (never updated yet)."""
+
+ def is_available(self) -> bool:
+ """Return `True` if the channel is available (not empty), `False` otherwise.
+
+ Subclasses should override this method to provide a more efficient
+ implementation than calling `get()` and catching `EmptyChannelError`.
+ """
+ try:
+ self.get()
+ return True
+ except EmptyChannelError:
+ return False
+
+ # write methods
+
+ @abstractmethod
+ def update(self, values: Sequence[Update]) -> bool:
+ """Update the channel's value with the given sequence of updates.
+ The order of the updates in the sequence is arbitrary.
+ This method is called by Pregel for all channels at the end of each step.
+
+ If there are no updates, it is called with an empty sequence.
+
+ Raises `InvalidUpdateError` if the sequence of updates is invalid.
+
+ Returns `True` if the channel was updated, `False` otherwise."""
+
+ def consume(self) -> bool:
+ """Notify the channel that a subscribed task ran.
+
+ By default, no-op.
+
+ A channel can use this method to modify its state, preventing the value from being consumed again.
+
+ Returns `True` if the channel was updated, `False` otherwise.
+ """
+ return False
+
+ def finish(self) -> bool:
+ """Notify the channel that the Pregel run is finishing.
+
+ By default, no-op.
+
+ A channel can use this method to modify its state, preventing finish.
+
+ Returns `True` if the channel was updated, `False` otherwise.
+ """
+ return False
diff --git a/langgraph/source/libs/langgraph/langgraph/channels/binop.py b/langgraph/source/libs/langgraph/langgraph/channels/binop.py
new file mode 100644
index 0000000000000000000000000000000000000000..b33ea52fa4a3b196854faf1cc84aef6908b86835
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/channels/binop.py
@@ -0,0 +1,134 @@
+import collections.abc
+from collections.abc import Callable, Sequence
+from typing import Any, Generic
+
+from typing_extensions import NotRequired, Required, Self
+
+from langgraph._internal._constants import OVERWRITE
+from langgraph._internal._typing import MISSING
+from langgraph.channels.base import BaseChannel, Value
+from langgraph.errors import (
+ EmptyChannelError,
+ ErrorCode,
+ InvalidUpdateError,
+ create_error_message,
+)
+from langgraph.types import Overwrite
+
+__all__ = ("BinaryOperatorAggregate",)
+
+
+# Adapted from typing_extensions
+def _strip_extras(t): # type: ignore[no-untyped-def]
+ """Strips Annotated, Required and NotRequired from a given type."""
+ if hasattr(t, "__origin__"):
+ return _strip_extras(t.__origin__)
+ if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired):
+ return _strip_extras(t.__args__[0])
+
+ return t
+
+
+def _get_overwrite(value: Any) -> tuple[bool, Any]:
+ """Inspects the given value and returns (is_overwrite, overwrite_value)."""
+ if isinstance(value, Overwrite):
+ return True, value.value
+ if isinstance(value, dict) and set(value.keys()) == {OVERWRITE}:
+ return True, value[OVERWRITE]
+ return False, None
+
+
+class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
+ """Stores the result of applying a binary operator to the current value and each new value.
+
+ ```python
+ import operator
+
+ total = Channels.BinaryOperatorAggregate(int, operator.add)
+ ```
+ """
+
+ __slots__ = ("value", "operator")
+
+ def __init__(self, typ: type[Value], operator: Callable[[Value, Value], Value]):
+ super().__init__(typ)
+ self.operator = operator
+ # special forms from typing or collections.abc are not instantiable
+ # so we need to replace them with their concrete counterparts
+ typ = _strip_extras(typ)
+ if typ in (collections.abc.Sequence, collections.abc.MutableSequence):
+ typ = list
+ if typ in (collections.abc.Set, collections.abc.MutableSet):
+ typ = set
+ if typ in (collections.abc.Mapping, collections.abc.MutableMapping):
+ typ = dict
+ try:
+ self.value = typ()
+ except Exception:
+ self.value = MISSING
+
+ def __eq__(self, value: object) -> bool:
+ return isinstance(value, BinaryOperatorAggregate) and (
+ value.operator is self.operator
+ if value.operator.__name__ != ""
+ and self.operator.__name__ != ""
+ else True
+ )
+
+ @property
+ def ValueType(self) -> type[Value]:
+ """The type of the value stored in the channel."""
+ return self.typ
+
+ @property
+ def UpdateType(self) -> type[Value]:
+ """The type of the update received by the channel."""
+ return self.typ
+
+ def copy(self) -> Self:
+ """Return a copy of the channel."""
+ empty = self.__class__(self.typ, self.operator)
+ empty.key = self.key
+ empty.value = self.value
+ return empty
+
+ def from_checkpoint(self, checkpoint: Value) -> Self:
+ empty = self.__class__(self.typ, self.operator)
+ empty.key = self.key
+ if checkpoint is not MISSING:
+ empty.value = checkpoint
+ return empty
+
+ def update(self, values: Sequence[Value]) -> bool:
+ if not values:
+ return False
+ if self.value is MISSING:
+ self.value = values[0]
+ values = values[1:]
+ seen_overwrite: bool = False
+ for value in values:
+ is_overwrite, overwrite_value = _get_overwrite(value)
+ if is_overwrite:
+ if seen_overwrite:
+ msg = create_error_message(
+ message="Can receive only one Overwrite value per super-step.",
+ error_code=ErrorCode.INVALID_CONCURRENT_GRAPH_UPDATE,
+ )
+ raise InvalidUpdateError(msg)
+ self.value = overwrite_value
+ seen_overwrite = True
+ continue
+ if not seen_overwrite:
+ self.value = self.operator(self.value, value)
+ return True
+
+ def get(self) -> Value:
+ if self.value is MISSING:
+ raise EmptyChannelError()
+ return self.value
+
+ def is_available(self) -> bool:
+ return self.value is not MISSING
+
+ def checkpoint(self) -> Value:
+ return self.value
diff --git a/langgraph/source/libs/langgraph/langgraph/channels/ephemeral_value.py b/langgraph/source/libs/langgraph/langgraph/channels/ephemeral_value.py
new file mode 100644
index 0000000000000000000000000000000000000000..108588d0b8276d0b818d53cd908d0563e7181035
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/channels/ephemeral_value.py
@@ -0,0 +1,79 @@
+from __future__ import annotations
+
+from collections.abc import Sequence
+from typing import Any, Generic
+
+from typing_extensions import Self
+
+from langgraph._internal._typing import MISSING
+from langgraph.channels.base import BaseChannel, Value
+from langgraph.errors import EmptyChannelError, InvalidUpdateError
+
+__all__ = ("EphemeralValue",)
+
+
+class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]):
+ """Stores the value received in the step immediately preceding, clears after."""
+
+ __slots__ = ("value", "guard")
+
+ value: Value | Any
+ guard: bool
+
+ def __init__(self, typ: Any, guard: bool = True) -> None:
+ super().__init__(typ)
+ self.guard = guard
+ self.value = MISSING
+
+ def __eq__(self, value: object) -> bool:
+ return isinstance(value, EphemeralValue) and value.guard == self.guard
+
+ @property
+ def ValueType(self) -> type[Value]:
+ """The type of the value stored in the channel."""
+ return self.typ
+
+ @property
+ def UpdateType(self) -> type[Value]:
+ """The type of the update received by the channel."""
+ return self.typ
+
+ def copy(self) -> Self:
+ """Return a copy of the channel."""
+ empty = self.__class__(self.typ, self.guard)
+ empty.key = self.key
+ empty.value = self.value
+ return empty
+
+ def from_checkpoint(self, checkpoint: Value) -> Self:
+ empty = self.__class__(self.typ, self.guard)
+ empty.key = self.key
+ if checkpoint is not MISSING:
+ empty.value = checkpoint
+ return empty
+
+ def update(self, values: Sequence[Value]) -> bool:
+ if len(values) == 0:
+ if self.value is not MISSING:
+ self.value = MISSING
+ return True
+ else:
+ return False
+ if len(values) != 1 and self.guard:
+ raise InvalidUpdateError(
+ f"At key '{self.key}': EphemeralValue(guard=True) can receive only one value per step. Use guard=False if you want to store any one of multiple values."
+ )
+
+ self.value = values[-1]
+ return True
+
+ def get(self) -> Value:
+ if self.value is MISSING:
+ raise EmptyChannelError()
+ return self.value
+
+ def is_available(self) -> bool:
+ return self.value is not MISSING
+
+ def checkpoint(self) -> Value:
+ return self.value
diff --git a/langgraph/source/libs/langgraph/langgraph/channels/last_value.py b/langgraph/source/libs/langgraph/langgraph/channels/last_value.py
new file mode 100644
index 0000000000000000000000000000000000000000..54caac7581ea5e1929f89e2b8a21653621dfac37
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/channels/last_value.py
@@ -0,0 +1,151 @@
+from __future__ import annotations
+
+from collections.abc import Sequence
+from typing import Any, Generic
+
+from typing_extensions import Self
+
+from langgraph._internal._typing import MISSING
+from langgraph.channels.base import BaseChannel, Value
+from langgraph.errors import (
+ EmptyChannelError,
+ ErrorCode,
+ InvalidUpdateError,
+ create_error_message,
+)
+
+__all__ = ("LastValue", "LastValueAfterFinish")
+
+
+class LastValue(Generic[Value], BaseChannel[Value, Value, Value]):
+ """Stores the last value received, can receive at most one value per step."""
+
+ __slots__ = ("value",)
+
+ value: Value | Any
+
+ def __init__(self, typ: Any, key: str = "") -> None:
+ super().__init__(typ, key)
+ self.value = MISSING
+
+ def __eq__(self, value: object) -> bool:
+ return isinstance(value, LastValue)
+
+ @property
+ def ValueType(self) -> type[Value]:
+ """The type of the value stored in the channel."""
+ return self.typ
+
+ @property
+ def UpdateType(self) -> type[Value]:
+ """The type of the update received by the channel."""
+ return self.typ
+
+ def copy(self) -> Self:
+ """Return a copy of the channel."""
+ empty = self.__class__(self.typ, self.key)
+ empty.value = self.value
+ return empty
+
+ def from_checkpoint(self, checkpoint: Value) -> Self:
+ empty = self.__class__(self.typ, self.key)
+ if checkpoint is not MISSING:
+ empty.value = checkpoint
+ return empty
+
+ def update(self, values: Sequence[Value]) -> bool:
+ if len(values) == 0:
+ return False
+ if len(values) != 1:
+ msg = create_error_message(
+ message=f"At key '{self.key}': Can receive only one value per step. Use an Annotated key to handle multiple values.",
+ error_code=ErrorCode.INVALID_CONCURRENT_GRAPH_UPDATE,
+ )
+ raise InvalidUpdateError(msg)
+
+ self.value = values[-1]
+ return True
+
+ def get(self) -> Value:
+ if self.value is MISSING:
+ raise EmptyChannelError()
+ return self.value
+
+ def is_available(self) -> bool:
+ return self.value is not MISSING
+
+ def checkpoint(self) -> Value:
+ return self.value
+
+
+class LastValueAfterFinish(
+ Generic[Value], BaseChannel[Value, Value, tuple[Value, bool]]
+):
+ """Stores the last value received, but only made available after finish().
+ Once made available, clears the value."""
+
+ __slots__ = ("value", "finished")
+
+ value: Value | Any
+ finished: bool
+
+ def __init__(self, typ: Any, key: str = "") -> None:
+ super().__init__(typ, key)
+ self.value = MISSING
+ self.finished = False
+
+ def __eq__(self, value: object) -> bool:
+ return isinstance(value, LastValueAfterFinish)
+
+ @property
+ def ValueType(self) -> type[Value]:
+ """The type of the value stored in the channel."""
+ return self.typ
+
+ @property
+ def UpdateType(self) -> type[Value]:
+ """The type of the update received by the channel."""
+ return self.typ
+
+ def checkpoint(self) -> tuple[Value | Any, bool] | Any:
+ if self.value is MISSING:
+ return MISSING
+ return (self.value, self.finished)
+
+ def from_checkpoint(self, checkpoint: tuple[Value | Any, bool] | Any) -> Self:
+ empty = self.__class__(self.typ)
+ empty.key = self.key
+ if checkpoint is not MISSING:
+ empty.value, empty.finished = checkpoint
+ return empty
+
+ def update(self, values: Sequence[Value | Any]) -> bool:
+ if len(values) == 0:
+ return False
+
+ self.finished = False
+ self.value = values[-1]
+ return True
+
+ def consume(self) -> bool:
+ if self.finished:
+ self.finished = False
+ self.value = MISSING
+ return True
+
+ return False
+
+ def finish(self) -> bool:
+ if not self.finished and self.value is not MISSING:
+ self.finished = True
+ return True
+ else:
+ return False
+
+ def get(self) -> Value:
+ if self.value is MISSING or not self.finished:
+ raise EmptyChannelError()
+ return self.value
+
+ def is_available(self) -> bool:
+ return self.value is not MISSING and self.finished
diff --git a/langgraph/source/libs/langgraph/langgraph/channels/named_barrier_value.py b/langgraph/source/libs/langgraph/langgraph/channels/named_barrier_value.py
new file mode 100644
index 0000000000000000000000000000000000000000..d45644110f47956d7adb5ef9c2138eae597dcf3c
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/channels/named_barrier_value.py
@@ -0,0 +1,167 @@
+from collections.abc import Sequence
+from typing import Generic
+
+from typing_extensions import Self
+
+from langgraph._internal._typing import MISSING
+from langgraph.channels.base import BaseChannel, Value
+from langgraph.errors import EmptyChannelError, InvalidUpdateError
+
+__all__ = ("NamedBarrierValue", "NamedBarrierValueAfterFinish")
+
+
+class NamedBarrierValue(Generic[Value], BaseChannel[Value, Value, set[Value]]):
+ """A channel that waits until all named values are received before making the value available."""
+
+ __slots__ = ("names", "seen")
+
+ names: set[Value]
+ seen: set[Value]
+
+ def __init__(self, typ: type[Value], names: set[Value]) -> None:
+ super().__init__(typ)
+ self.names = names
+ self.seen: set[str] = set()
+
+ def __eq__(self, value: object) -> bool:
+ return isinstance(value, NamedBarrierValue) and value.names == self.names
+
+ @property
+ def ValueType(self) -> type[Value]:
+ """The type of the value stored in the channel."""
+ return self.typ
+
+ @property
+ def UpdateType(self) -> type[Value]:
+ """The type of the update received by the channel."""
+ return self.typ
+
+ def copy(self) -> Self:
+ """Return a copy of the channel."""
+ empty = self.__class__(self.typ, self.names)
+ empty.key = self.key
+ empty.seen = self.seen.copy()
+ return empty
+
+ def checkpoint(self) -> set[Value]:
+ return self.seen
+
+ def from_checkpoint(self, checkpoint: set[Value]) -> Self:
+ empty = self.__class__(self.typ, self.names)
+ empty.key = self.key
+ if checkpoint is not MISSING:
+ empty.seen = checkpoint
+ return empty
+
+ def update(self, values: Sequence[Value]) -> bool:
+ updated = False
+ for value in values:
+ if value in self.names:
+ if value not in self.seen:
+ self.seen.add(value)
+ updated = True
+ else:
+ raise InvalidUpdateError(
+ f"At key '{self.key}': Value {value} not in {self.names}"
+ )
+ return updated
+
+ def get(self) -> Value:
+ if self.seen != self.names:
+ raise EmptyChannelError()
+ return None
+
+ def is_available(self) -> bool:
+ return self.seen == self.names
+
+ def consume(self) -> bool:
+ if self.seen == self.names:
+ self.seen = set()
+ return True
+ return False
+
+
+class NamedBarrierValueAfterFinish(
+ Generic[Value], BaseChannel[Value, Value, set[Value]]
+):
+ """A channel that waits until all named values are received before making the value ready to be made available. It is only made available after finish() is called."""
+
+ __slots__ = ("names", "seen", "finished")
+
+ names: set[Value]
+ seen: set[Value]
+
+ def __init__(self, typ: type[Value], names: set[Value]) -> None:
+ super().__init__(typ)
+ self.names = names
+ self.seen: set[str] = set()
+ self.finished = False
+
+ def __eq__(self, value: object) -> bool:
+ return (
+ isinstance(value, NamedBarrierValueAfterFinish)
+ and value.names == self.names
+ )
+
+ @property
+ def ValueType(self) -> type[Value]:
+ """The type of the value stored in the channel."""
+ return self.typ
+
+ @property
+ def UpdateType(self) -> type[Value]:
+ """The type of the update received by the channel."""
+ return self.typ
+
+ def copy(self) -> Self:
+ """Return a copy of the channel."""
+ empty = self.__class__(self.typ, self.names)
+ empty.key = self.key
+ empty.seen = self.seen.copy()
+ empty.finished = self.finished
+ return empty
+
+ def checkpoint(self) -> tuple[set[Value], bool]:
+ return (self.seen, self.finished)
+
+ def from_checkpoint(self, checkpoint: tuple[set[Value], bool]) -> Self:
+ empty = self.__class__(self.typ, self.names)
+ empty.key = self.key
+ if checkpoint is not MISSING:
+ empty.seen, empty.finished = checkpoint
+ return empty
+
+ def update(self, values: Sequence[Value]) -> bool:
+ updated = False
+ for value in values:
+ if value in self.names:
+ if value not in self.seen:
+ self.seen.add(value)
+ updated = True
+ else:
+ raise InvalidUpdateError(
+ f"At key '{self.key}': Value {value} not in {self.names}"
+ )
+ return updated
+
+ def get(self) -> Value:
+ if not self.finished or self.seen != self.names:
+ raise EmptyChannelError()
+ return None
+
+ def is_available(self) -> bool:
+ return self.finished and self.seen == self.names
+
+ def consume(self) -> bool:
+ if self.finished and self.seen == self.names:
+ self.finished = False
+ self.seen = set()
+ return True
+ return False
+
+ def finish(self) -> bool:
+ if not self.finished and self.seen == self.names:
+ self.finished = True
+ return True
+ else:
+ return False
diff --git a/langgraph/source/libs/langgraph/langgraph/channels/topic.py b/langgraph/source/libs/langgraph/langgraph/channels/topic.py
new file mode 100644
index 0000000000000000000000000000000000000000..4f17d7c9e8152c9bbe7b779f404ad2f7701101dd
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/channels/topic.py
@@ -0,0 +1,94 @@
+from __future__ import annotations
+
+from collections.abc import Iterator, Sequence
+from typing import Any, Generic
+
+from typing_extensions import Self
+
+from langgraph._internal._typing import MISSING
+from langgraph.channels.base import BaseChannel, Value
+from langgraph.errors import EmptyChannelError
+
+__all__ = ("Topic",)
+
+
+def _flatten(values: Sequence[Value | list[Value]]) -> Iterator[Value]:
+ for value in values:
+ if isinstance(value, list):
+ yield from value
+ else:
+ yield value
+
+
+class Topic(
+ Generic[Value],
+ BaseChannel[Sequence[Value], Value | list[Value], list[Value]],
+):
+ """A configurable PubSub Topic.
+
+ Args:
+ typ: The type of the value stored in the channel.
+ accumulate: Whether to accumulate values across steps. If `False`, the channel will be emptied after each step.
+ """
+
+ __slots__ = ("values", "accumulate")
+
+ def __init__(self, typ: type[Value], accumulate: bool = False) -> None:
+ super().__init__(typ)
+ # attrs
+ self.accumulate = accumulate
+ # state
+ self.values = list[Value]()
+
+ def __eq__(self, value: object) -> bool:
+ return isinstance(value, Topic) and value.accumulate == self.accumulate
+
+ @property
+ def ValueType(self) -> Any:
+ """The type of the value stored in the channel."""
+ return Sequence[self.typ] # type: ignore[name-defined]
+
+ @property
+ def UpdateType(self) -> Any:
+ """The type of the update received by the channel."""
+ return self.typ | list[self.typ] # type: ignore[name-defined]
+
+ def copy(self) -> Self:
+ """Return a copy of the channel."""
+ empty = self.__class__(self.typ, self.accumulate)
+ empty.key = self.key
+ empty.values = self.values.copy()
+ return empty
+
+ def checkpoint(self) -> list[Value]:
+ return self.values
+
+ def from_checkpoint(self, checkpoint: list[Value]) -> Self:
+ empty = self.__class__(self.typ, self.accumulate)
+ empty.key = self.key
+ if checkpoint is not MISSING:
+ if isinstance(checkpoint, tuple):
+ # backwards compatibility
+ empty.values = checkpoint[1]
+ else:
+ empty.values = checkpoint
+ return empty
+
+ def update(self, values: Sequence[Value | list[Value]]) -> bool:
+ updated = False
+ if not self.accumulate:
+ updated = bool(self.values)
+ self.values = list[Value]()
+ if flat_values := tuple(_flatten(values)):
+ updated = True
+ self.values.extend(flat_values)
+ return updated
+
+ def get(self) -> Sequence[Value]:
+ if self.values:
+ return list(self.values)
+ else:
+ raise EmptyChannelError
+
+ def is_available(self) -> bool:
+ return bool(self.values)
diff --git a/langgraph/source/libs/langgraph/langgraph/channels/untracked_value.py b/langgraph/source/libs/langgraph/langgraph/channels/untracked_value.py
new file mode 100644
index 0000000000000000000000000000000000000000..bcd55186b9ae7199205e8ad527adbb3bbf660e49
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/channels/untracked_value.py
@@ -0,0 +1,73 @@
+from __future__ import annotations
+
+from collections.abc import Sequence
+from typing import Any, Generic
+
+from typing_extensions import Self
+
+from langgraph._internal._typing import MISSING
+from langgraph.channels.base import BaseChannel, Value
+from langgraph.errors import EmptyChannelError, InvalidUpdateError
+
+__all__ = ("UntrackedValue",)
+
+
+class UntrackedValue(Generic[Value], BaseChannel[Value, Value, Value]):
+ """Stores the last value received, never checkpointed."""
+
+ __slots__ = ("value", "guard")
+
+ guard: bool
+ value: Value | Any
+
+ def __init__(self, typ: type[Value], guard: bool = True) -> None:
+ super().__init__(typ)
+ self.guard = guard
+ self.value = MISSING
+
+ def __eq__(self, value: object) -> bool:
+ return isinstance(value, UntrackedValue) and value.guard == self.guard
+
+ @property
+ def ValueType(self) -> type[Value]:
+ """The type of the value stored in the channel."""
+ return self.typ
+
+ @property
+ def UpdateType(self) -> type[Value]:
+ """The type of the update received by the channel."""
+ return self.typ
+
+ def copy(self) -> Self:
+ """Return a copy of the channel."""
+ empty = self.__class__(self.typ, self.guard)
+ empty.key = self.key
+ empty.value = self.value
+ return empty
+
+ def checkpoint(self) -> Value | Any:
+ return MISSING
+
+ def from_checkpoint(self, checkpoint: Value) -> Self:
+ empty = self.__class__(self.typ, self.guard)
+ empty.key = self.key
+ return empty
+
+ def update(self, values: Sequence[Value]) -> bool:
+ if len(values) == 0:
+ return False
+ if len(values) != 1 and self.guard:
+ raise InvalidUpdateError(
+ f"At key '{self.key}': UntrackedValue(guard=True) can receive only one value per step. Use guard=False if you want to store any one of multiple values."
+ )
+
+ self.value = values[-1]
+ return True
+
+ def get(self) -> Value:
+ if self.value is MISSING:
+ raise EmptyChannelError()
+ return self.value
+
+ def is_available(self) -> bool:
+ return self.value is not MISSING
diff --git a/langgraph/source/libs/langgraph/langgraph/config.py b/langgraph/source/libs/langgraph/langgraph/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..c8a321d8399d1fe2965d58b3a6f86a3ff602be94
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/config.py
@@ -0,0 +1,196 @@
+import asyncio
+import sys
+from typing import Any
+
+from langchain_core.runnables import RunnableConfig
+from langchain_core.runnables.config import var_child_runnable_config
+from langgraph.store.base import BaseStore
+
+from langgraph._internal._constants import CONF, CONFIG_KEY_RUNTIME
+from langgraph.types import StreamWriter
+
+
+def _no_op_stream_writer(c: Any) -> None:
+ pass
+
+
+def get_config() -> RunnableConfig:
+ if sys.version_info < (3, 11):
+ try:
+ if asyncio.current_task():
+ raise RuntimeError(
+ "Python 3.11 or later required to use this in an async context"
+ )
+ except RuntimeError:
+ pass
+ if var_config := var_child_runnable_config.get():
+ return var_config
+ else:
+ raise RuntimeError("Called get_config outside of a runnable context")
+
+
+def get_store() -> BaseStore:
+ """Access LangGraph store from inside a graph node or entrypoint task at runtime.
+
+ Can be called from inside any [`StateGraph`][langgraph.graph.StateGraph] node or
+ functional API [`task`][langgraph.func.task], as long as the `StateGraph` or the [`entrypoint`][langgraph.func.entrypoint]
+ was initialized with a store, e.g.:
+
+ ```python
+ # with StateGraph
+ graph = (
+ StateGraph(...)
+ ...
+ .compile(store=store)
+ )
+
+ # or with entrypoint
+ @entrypoint(store=store)
+ def workflow(inputs):
+ ...
+ ```
+
+ !!! warning "Async with Python < 3.11"
+
+ If you are using Python < 3.11 and are running LangGraph asynchronously,
+ `get_store()` won't work since it uses [`contextvar`](https://docs.python.org/3/library/contextvars.html) propagation (only available in [Python >= 3.11](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task)).
+
+
+ Example: Using with `StateGraph`
+ ```python
+ from typing_extensions import TypedDict
+ from langgraph.graph import StateGraph, START
+ from langgraph.store.memory import InMemoryStore
+ from langgraph.config import get_store
+
+ store = InMemoryStore()
+ store.put(("values",), "foo", {"bar": 2})
+
+
+ class State(TypedDict):
+ foo: int
+
+
+ def my_node(state: State):
+ my_store = get_store()
+ stored_value = my_store.get(("values",), "foo").value["bar"]
+ return {"foo": stored_value + 1}
+
+
+ graph = (
+ StateGraph(State)
+ .add_node(my_node)
+ .add_edge(START, "my_node")
+ .compile(store=store)
+ )
+
+ graph.invoke({"foo": 1})
+ ```
+
+ ```pycon
+ {"foo": 3}
+ ```
+
+ Example: Using with functional API
+ ```python
+ from langgraph.func import entrypoint, task
+ from langgraph.store.memory import InMemoryStore
+ from langgraph.config import get_store
+
+ store = InMemoryStore()
+ store.put(("values",), "foo", {"bar": 2})
+
+
+ @task
+ def my_task(value: int):
+ my_store = get_store()
+ stored_value = my_store.get(("values",), "foo").value["bar"]
+ return stored_value + 1
+
+
+ @entrypoint(store=store)
+ def workflow(value: int):
+ return my_task(value).result()
+
+
+ workflow.invoke(1)
+ ```
+
+ ```pycon
+ 3
+ ```
+ """
+ return get_config()[CONF][CONFIG_KEY_RUNTIME].store
+
+
+def get_stream_writer() -> StreamWriter:
+ """Access LangGraph [`StreamWriter`][langgraph.types.StreamWriter] from inside a graph node or entrypoint task at runtime.
+
+ Can be called from inside any [`StateGraph`][langgraph.graph.StateGraph] node or
+ functional API [`task`][langgraph.func.task].
+
+ !!! warning "Async with Python < 3.11"
+
+ If you are using Python < 3.11 and are running LangGraph asynchronously,
+ `get_stream_writer()` won't work since it uses [`contextvar`](https://docs.python.org/3/library/contextvars.html) propagation (only available in [Python >= 3.11](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task)).
+
+ Example: Using with `StateGraph`
+ ```python
+ from typing_extensions import TypedDict
+ from langgraph.graph import StateGraph, START
+ from langgraph.config import get_stream_writer
+
+
+ class State(TypedDict):
+ foo: int
+
+
+ def my_node(state: State):
+ my_stream_writer = get_stream_writer()
+ my_stream_writer({"custom_data": "Hello!"})
+ return {"foo": state["foo"] + 1}
+
+
+ graph = (
+ StateGraph(State)
+ .add_node(my_node)
+ .add_edge(START, "my_node")
+ .compile(store=store)
+ )
+
+ for chunk in graph.stream({"foo": 1}, stream_mode="custom"):
+ print(chunk)
+ ```
+
+ ```pycon
+ {"custom_data": "Hello!"}
+ ```
+
+ Example: Using with functional API
+ ```python
+ from langgraph.func import entrypoint, task
+ from langgraph.config import get_stream_writer
+
+
+ @task
+ def my_task(value: int):
+ my_stream_writer = get_stream_writer()
+ my_stream_writer({"custom_data": "Hello!"})
+ return value + 1
+
+
+ @entrypoint(store=store)
+ def workflow(value: int):
+ return my_task(value).result()
+
+
+ for chunk in workflow.stream(1, stream_mode="custom"):
+ print(chunk)
+ ```
+
+ ```pycon
+ {"custom_data": "Hello!"}
+ ```
+ """
+ runtime = get_config()[CONF][CONFIG_KEY_RUNTIME]
+ return runtime.stream_writer
diff --git a/langgraph/source/libs/langgraph/langgraph/constants.py b/langgraph/source/libs/langgraph/langgraph/constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..5b7e52aae2d7c1c0824a475f71b9a45e985f254d
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/constants.py
@@ -0,0 +1,64 @@
+import sys
+from typing import Any
+from warnings import warn
+
+from langgraph._internal._constants import (
+ CONF,
+ CONFIG_KEY_CHECKPOINTER,
+ TASKS,
+)
+from langgraph.warnings import LangGraphDeprecatedSinceV10
+
+__all__ = (
+ "TAG_NOSTREAM",
+ "TAG_HIDDEN",
+ "START",
+ "END",
+ # retained for backwards compatibility (mostly langgraph-api), should be removed in v2 (or earlier)
+ "CONF",
+ "TASKS",
+ "CONFIG_KEY_CHECKPOINTER",
+)
+
+# --- Public constants ---
+TAG_NOSTREAM = sys.intern("nostream")
+"""Tag to disable streaming for a chat model."""
+TAG_HIDDEN = sys.intern("langsmith:hidden")
+"""Tag to hide a node/edge from certain tracing/streaming environments."""
+END = sys.intern("__end__")
+"""The last (maybe virtual) node in graph-style Pregel."""
+START = sys.intern("__start__")
+"""The first (maybe virtual) node in graph-style Pregel."""
+
+
+def __getattr__(name: str) -> Any:
+ if name in ["Send", "Interrupt"]:
+ warn(
+ f"Importing {name} from langgraph.constants is deprecated. "
+ f"Please use 'from langgraph.types import {name}' instead.",
+ LangGraphDeprecatedSinceV10,
+ stacklevel=2,
+ )
+
+ from importlib import import_module
+
+ module = import_module("langgraph.types")
+ return getattr(module, name)
+
+ try:
+ from importlib import import_module
+
+ private_constants = import_module("langgraph._internal._constants")
+ attr = getattr(private_constants, name)
+ warn(
+ f"Importing {name} from langgraph.constants is deprecated. "
+ f"This constant is now private and should not be used directly. "
+ "Please let the LangGraph team know if you need this value.",
+ LangGraphDeprecatedSinceV10,
+ stacklevel=2,
+ )
+ return attr
+ except AttributeError:
+ pass
+
+ raise AttributeError(f"module has no attribute '{name}'")
diff --git a/langgraph/source/libs/langgraph/langgraph/errors.py b/langgraph/source/libs/langgraph/langgraph/errors.py
new file mode 100644
index 0000000000000000000000000000000000000000..fb648879e6ffea0a247c771e6e201488e5b12ca1
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/errors.py
@@ -0,0 +1,127 @@
+from __future__ import annotations
+
+from collections.abc import Sequence
+from enum import Enum
+from typing import Any
+from warnings import warn
+
+# EmptyChannelError is re-exported from langgraph.channels.base
+from langgraph.checkpoint.base import EmptyChannelError # noqa: F401
+from typing_extensions import deprecated
+
+from langgraph.types import Command, Interrupt
+from langgraph.warnings import LangGraphDeprecatedSinceV10
+
+__all__ = (
+ "EmptyChannelError",
+ "ErrorCode",
+ "GraphRecursionError",
+ "InvalidUpdateError",
+ "GraphBubbleUp",
+ "GraphInterrupt",
+ "NodeInterrupt",
+ "ParentCommand",
+ "EmptyInputError",
+ "TaskNotFound",
+)
+
+
+class ErrorCode(Enum):
+ GRAPH_RECURSION_LIMIT = "GRAPH_RECURSION_LIMIT"
+ INVALID_CONCURRENT_GRAPH_UPDATE = "INVALID_CONCURRENT_GRAPH_UPDATE"
+ INVALID_GRAPH_NODE_RETURN_VALUE = "INVALID_GRAPH_NODE_RETURN_VALUE"
+ MULTIPLE_SUBGRAPHS = "MULTIPLE_SUBGRAPHS"
+ INVALID_CHAT_HISTORY = "INVALID_CHAT_HISTORY"
+
+
+def create_error_message(*, message: str, error_code: ErrorCode) -> str:
+ return (
+ f"{message}\n"
+ "For troubleshooting, visit: https://docs.langchain.com/oss/python/langgraph/"
+ f"errors/{error_code.value}"
+ )
+
+
+class GraphRecursionError(RecursionError):
+ """Raised when the graph has exhausted the maximum number of steps.
+
+ This prevents infinite loops. To increase the maximum number of steps,
+ run your graph with a config specifying a higher `recursion_limit`.
+
+ Troubleshooting guides:
+
+ - [`GRAPH_RECURSION_LIMIT`](https://docs.langchain.com/oss/python/langgraph/GRAPH_RECURSION_LIMIT)
+
+ Examples:
+
+ graph = builder.compile()
+ graph.invoke(
+ {"messages": [("user", "Hello, world!")]},
+ # The config is the second positional argument
+ {"recursion_limit": 1000},
+ )
+ """
+
+ pass
+
+
+class InvalidUpdateError(Exception):
+ """Raised when attempting to update a channel with an invalid set of updates.
+
+ Troubleshooting guides:
+
+ - [`INVALID_CONCURRENT_GRAPH_UPDATE`](https://docs.langchain.com/oss/python/langgraph/INVALID_CONCURRENT_GRAPH_UPDATE)
+ - [`INVALID_GRAPH_NODE_RETURN_VALUE`](https://docs.langchain.com/oss/python/langgraph/INVALID_GRAPH_NODE_RETURN_VALUE)
+ """
+
+ pass
+
+
+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."""
+
+ def __init__(self, interrupts: Sequence[Interrupt] = ()) -> None:
+ super().__init__(interrupts)
+
+
+@deprecated(
+ "NodeInterrupt is deprecated. Please use [`interrupt`][langgraph.types.interrupt] instead.",
+ category=None,
+)
+class NodeInterrupt(GraphInterrupt):
+ """Raised by a node to interrupt execution."""
+
+ def __init__(self, value: Any, id: str | None = None) -> None:
+ warn(
+ "NodeInterrupt is deprecated. Please use `langgraph.types.interrupt` instead.",
+ LangGraphDeprecatedSinceV10,
+ stacklevel=2,
+ )
+ if id is None:
+ super().__init__([Interrupt(value=value)])
+ else:
+ super().__init__([Interrupt(value=value, id=id)])
+
+
+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."""
+
+ pass
+
+
+class TaskNotFound(Exception):
+ """Raised when the executor is unable to find a task (for distributed mode)."""
+
+ pass
diff --git a/langgraph/source/libs/langgraph/langgraph/func/__init__.py b/langgraph/source/libs/langgraph/langgraph/func/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..d1587077d8fc7aaca0b7af76633134ce2afcddd2
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/func/__init__.py
@@ -0,0 +1,563 @@
+from __future__ import annotations
+
+import functools
+import inspect
+import warnings
+from collections.abc import Awaitable, Callable, Sequence
+from dataclasses import dataclass
+from typing import (
+ Any,
+ Generic,
+ TypeVar,
+ cast,
+ get_args,
+ get_origin,
+ overload,
+)
+
+from langgraph.cache.base import BaseCache
+from langgraph.checkpoint.base import BaseCheckpointSaver
+from langgraph.store.base import BaseStore
+from typing_extensions import Unpack
+
+from langgraph._internal._constants import CACHE_NS_WRITES, PREVIOUS
+from langgraph._internal._typing import MISSING, DeprecatedKwargs
+from langgraph.channels.ephemeral_value import EphemeralValue
+from langgraph.channels.last_value import LastValue
+from langgraph.constants import END, START
+from langgraph.pregel import Pregel
+from langgraph.pregel._call import (
+ P,
+ SyncAsyncFuture,
+ T,
+ call,
+ get_runnable_for_entrypoint,
+ identifier,
+)
+from langgraph.pregel._read import PregelNode
+from langgraph.pregel._write import ChannelWrite, ChannelWriteEntry
+from langgraph.types import _DC_KWARGS, CachePolicy, RetryPolicy, StreamMode
+from langgraph.typing import ContextT
+from langgraph.warnings import LangGraphDeprecatedSinceV05, LangGraphDeprecatedSinceV10
+
+__all__ = ("task", "entrypoint")
+
+
+class _TaskFunction(Generic[P, T]):
+ def __init__(
+ self,
+ func: Callable[P, Awaitable[T]] | Callable[P, T],
+ *,
+ retry_policy: Sequence[RetryPolicy],
+ cache_policy: CachePolicy[Callable[P, str | bytes]] | None = None,
+ name: str | None = None,
+ ) -> None:
+ if name is not None:
+ if hasattr(func, "__func__"):
+ # handle class methods
+ # NOTE: we're modifying the instance method to avoid modifying
+ # the original class method in case it's shared across multiple tasks
+ instance_method = functools.partial(func.__func__, func.__self__) # type: ignore [union-attr]
+ instance_method.__name__ = name # type: ignore [attr-defined]
+ func = instance_method
+ else:
+ # handle regular functions / partials / callable classes, etc.
+ func.__name__ = name
+ self.func = func
+ self.retry_policy = retry_policy
+ self.cache_policy = cache_policy
+ functools.update_wrapper(self, func)
+
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> SyncAsyncFuture[T]:
+ return call(
+ self.func,
+ retry_policy=self.retry_policy,
+ cache_policy=self.cache_policy,
+ *args,
+ **kwargs,
+ )
+
+ def clear_cache(self, cache: BaseCache) -> None:
+ """Clear the cache for this task."""
+ if self.cache_policy is not None:
+ cache.clear(((CACHE_NS_WRITES, identifier(self.func) or "__dynamic__"),))
+
+ async def aclear_cache(self, cache: BaseCache) -> None:
+ """Clear the cache for this task."""
+ if self.cache_policy is not None:
+ await cache.aclear(
+ ((CACHE_NS_WRITES, identifier(self.func) or "__dynamic__"),)
+ )
+
+
+@overload
+def task(
+ __func_or_none__: None = None,
+ *,
+ name: str | None = None,
+ retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
+ cache_policy: CachePolicy[Callable[P, str | bytes]] | None = None,
+ **kwargs: Unpack[DeprecatedKwargs],
+) -> Callable[
+ [Callable[P, Awaitable[T]] | Callable[P, T]],
+ _TaskFunction[P, T],
+]: ...
+
+
+@overload
+def task(__func_or_none__: Callable[P, Awaitable[T]]) -> _TaskFunction[P, T]: ...
+
+
+@overload
+def task(__func_or_none__: Callable[P, T]) -> _TaskFunction[P, T]: ...
+
+
+def task(
+ __func_or_none__: Callable[P, Awaitable[T]] | Callable[P, T] | None = None,
+ *,
+ name: str | None = None,
+ retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
+ cache_policy: CachePolicy[Callable[P, str | bytes]] | None = None,
+ **kwargs: Unpack[DeprecatedKwargs],
+) -> (
+ Callable[[Callable[P, Awaitable[T]] | Callable[P, T]], _TaskFunction[P, T]]
+ | _TaskFunction[P, T]
+):
+ """Define a LangGraph task using the `task` decorator.
+
+ !!! important "Requires python 3.11 or higher for async functions"
+ The `task` decorator supports both sync and async functions. To use async
+ functions, ensure that you are using Python 3.11 or higher.
+
+ Tasks can only be called from within an [`entrypoint`][langgraph.func.entrypoint] or
+ from within a `StateGraph`. A task can be called like a regular function with the
+ following differences:
+
+ - When a checkpointer is enabled, the function inputs and outputs must be serializable.
+ - The decorated function can only be called from within an entrypoint or `StateGraph`.
+ - Calling the function produces a future. This makes it easy to parallelize tasks.
+
+ Args:
+ name: An optional name for the task. If not provided, the function name will be used.
+ retry_policy: An optional retry policy (or list of policies) to use for the task in case of a failure.
+ cache_policy: An optional cache policy to use for the task. This allows caching of the task results.
+
+ Returns:
+ A callable function when used as a decorator.
+
+ Example: Sync Task
+ ```python
+ from langgraph.func import entrypoint, task
+
+
+ @task
+ def add_one_task(a: int) -> int:
+ return a + 1
+
+
+ @entrypoint()
+ def add_one(numbers: list[int]) -> list[int]:
+ futures = [add_one_task(n) for n in numbers]
+ results = [f.result() for f in futures]
+ return results
+
+
+ # Call the entrypoint
+ add_one.invoke([1, 2, 3]) # Returns [2, 3, 4]
+ ```
+
+ Example: Async Task
+ ```python
+ import asyncio
+ from langgraph.func import entrypoint, task
+
+
+ @task
+ async def add_one_task(a: int) -> int:
+ return a + 1
+
+
+ @entrypoint()
+ async def add_one(numbers: list[int]) -> list[int]:
+ futures = [add_one_task(n) for n in numbers]
+ return asyncio.gather(*futures)
+
+
+ # Call the entrypoint
+ await add_one.ainvoke([1, 2, 3]) # Returns [2, 3, 4]
+ ```
+ """
+ if (retry := kwargs.get("retry", MISSING)) is not MISSING:
+ warnings.warn(
+ "`retry` is deprecated and will be removed. Please use `retry_policy` instead.",
+ category=LangGraphDeprecatedSinceV05,
+ stacklevel=2,
+ )
+ if retry_policy is None:
+ retry_policy = retry # type: ignore[assignment]
+
+ retry_policies: Sequence[RetryPolicy] = (
+ ()
+ if retry_policy is None
+ else (retry_policy,)
+ if isinstance(retry_policy, RetryPolicy)
+ else retry_policy
+ )
+
+ def decorator(
+ func: Callable[P, Awaitable[T]] | Callable[P, T],
+ ) -> Callable[P, SyncAsyncFuture[T]]:
+ return _TaskFunction(
+ func, retry_policy=retry_policies, cache_policy=cache_policy, name=name
+ )
+
+ if __func_or_none__ is not None:
+ return decorator(__func_or_none__)
+
+ return decorator
+
+
+R = TypeVar("R")
+S = TypeVar("S")
+
+
+# The decorator was wrapped in a class to support the `final` attribute.
+# In this form, the `final` attribute should play nicely with IDE autocompletion,
+# and type checking tools.
+# In addition, we'll be able to surface this information in the API Reference.
+class entrypoint(Generic[ContextT]):
+ """Define a LangGraph workflow using the `entrypoint` decorator.
+
+ ### Function signature
+
+ The decorated function must accept a **single parameter**, which serves as the input
+ to the function. This input parameter can be of any type. Use a dictionary
+ to pass **multiple parameters** to the function.
+
+ ### Injectable parameters
+
+ The decorated function can request access to additional parameters
+ that will be injected automatically at run time. These parameters include:
+
+ | Parameter | Description |
+ |------------------|------------------------------------------------------------------------------------------------------|
+ | **`config`** | A configuration object (aka `RunnableConfig`) that holds run-time configuration values. |
+ | **`previous`** | The previous return value for the given thread (available only when a checkpointer is provided). |
+ | **`runtime`** | A `Runtime` object that contains information about the current run, including context, store, writer |
+
+ The entrypoint decorator can be applied to sync functions or async functions.
+
+ ### State management
+
+ The **`previous`** parameter can be used to access the return value of the previous
+ invocation of the entrypoint on the same thread id. This value is only available
+ when a checkpointer is provided.
+
+ If you want **`previous`** to be different from the return value, you can use the
+ `entrypoint.final` object to return a value while saving a different value to the
+ checkpoint.
+
+ Args:
+ checkpointer: Specify a checkpointer to create a workflow that can persist
+ its state across runs.
+ store: A generalized key-value store. Some implementations may support
+ semantic search capabilities through an optional `index` configuration.
+ cache: A cache to use for caching the results of the workflow.
+ context_schema: Specifies the schema for the context object that will be
+ passed to the workflow.
+ cache_policy: A cache policy to use for caching the results of the workflow.
+ retry_policy: A retry policy (or list of policies) to use for the workflow in case of a failure.
+
+ !!! warning "`config_schema` Deprecated"
+ The `config_schema` parameter is deprecated in v0.6.0 and support will be removed in v2.0.0.
+ Please use `context_schema` instead to specify the schema for run-scoped context.
+
+
+ Example: Using entrypoint and tasks
+ ```python
+ import time
+
+ from langgraph.func import entrypoint, task
+ from langgraph.types import interrupt, Command
+ from langgraph.checkpoint.memory import InMemorySaver
+
+ @task
+ def compose_essay(topic: str) -> str:
+ time.sleep(1.0) # Simulate slow operation
+ return f"An essay about {topic}"
+
+ @entrypoint(checkpointer=InMemorySaver())
+ def review_workflow(topic: str) -> dict:
+ \"\"\"Manages the workflow for generating and reviewing an essay.
+
+ The workflow includes:
+ 1. Generating an essay about the given topic.
+ 2. Interrupting the workflow for human review of the generated essay.
+
+ Upon resuming the workflow, compose_essay task will not be re-executed
+ as its result is cached by the checkpointer.
+
+ Args:
+ topic: The subject of the essay.
+
+ Returns:
+ dict: A dictionary containing the generated essay and the human review.
+ \"\"\"
+ essay_future = compose_essay(topic)
+ essay = essay_future.result()
+ human_review = interrupt({
+ \"question\": \"Please provide a review\",
+ \"essay\": essay
+ })
+ return {
+ \"essay\": essay,
+ \"review\": human_review,
+ }
+
+ # Example configuration for the workflow
+ config = {
+ \"configurable\": {
+ \"thread_id\": \"some_thread\"
+ }
+ }
+
+ # Topic for the essay
+ topic = \"cats\"
+
+ # Stream the workflow to generate the essay and await human review
+ for result in review_workflow.stream(topic, config):
+ print(result)
+
+ # Example human review provided after the interrupt
+ human_review = \"This essay is great.\"
+
+ # Resume the workflow with the provided human review
+ for result in review_workflow.stream(Command(resume=human_review), config):
+ print(result)
+ ```
+
+ Example: Accessing the previous return value
+ When a checkpointer is enabled the function can access the previous return value
+ of the previous invocation on the same thread id.
+
+ ```python
+ from typing import Optional
+
+ from langgraph.checkpoint.memory import MemorySaver
+
+ from langgraph.func import entrypoint
+
+
+ @entrypoint(checkpointer=InMemorySaver())
+ def my_workflow(input_data: str, previous: Optional[str] = None) -> str:
+ return "world"
+
+
+ config = {"configurable": {"thread_id": "some_thread"}}
+ my_workflow.invoke("hello", config)
+ ```
+
+ Example: Using `entrypoint.final` to save a value
+ The `entrypoint.final` object allows you to return a value while saving
+ a different value to the checkpoint. This value will be accessible
+ in the next invocation of the entrypoint via the `previous` parameter, as
+ long as the same thread id is used.
+
+ ```python
+ from typing import Any
+
+ from langgraph.checkpoint.memory import MemorySaver
+
+ from langgraph.func import entrypoint
+
+
+ @entrypoint(checkpointer=InMemorySaver())
+ def my_workflow(
+ number: int,
+ *,
+ previous: Any = None,
+ ) -> entrypoint.final[int, int]:
+ previous = previous or 0
+ # This will return the previous value to the caller, saving
+ # 2 * number to the checkpoint, which will be used in the next invocation
+ # for the `previous` parameter.
+ return entrypoint.final(value=previous, save=2 * number)
+
+
+ config = {"configurable": {"thread_id": "some_thread"}}
+
+ my_workflow.invoke(3, config) # 0 (previous was None)
+ my_workflow.invoke(1, config) # 6 (previous was 3 * 2 from the previous invocation)
+ ```
+ """
+
+ def __init__(
+ self,
+ checkpointer: BaseCheckpointSaver | None = None,
+ store: BaseStore | None = None,
+ cache: BaseCache | None = None,
+ context_schema: type[ContextT] | None = None,
+ cache_policy: CachePolicy | None = None,
+ retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
+ **kwargs: Unpack[DeprecatedKwargs],
+ ) -> None:
+ """Initialize the entrypoint decorator."""
+ if (config_schema := kwargs.get("config_schema", MISSING)) is not MISSING:
+ warnings.warn(
+ "`config_schema` is deprecated and will be removed. Please use `context_schema` instead.",
+ category=LangGraphDeprecatedSinceV10,
+ stacklevel=2,
+ )
+ if context_schema is None:
+ context_schema = cast(type[ContextT], config_schema)
+
+ if (retry := kwargs.get("retry", MISSING)) is not MISSING:
+ warnings.warn(
+ "`retry` is deprecated and will be removed. Please use `retry_policy` instead.",
+ category=LangGraphDeprecatedSinceV05,
+ stacklevel=2,
+ )
+ if retry_policy is None:
+ retry_policy = cast("RetryPolicy | Sequence[RetryPolicy]", retry)
+
+ self.checkpointer = checkpointer
+ self.store = store
+ self.cache = cache
+ self.cache_policy = cache_policy
+ self.retry_policy = retry_policy
+ self.context_schema = context_schema
+
+ @dataclass(**_DC_KWARGS)
+ class final(Generic[R, S]):
+ """A primitive that can be returned from an entrypoint.
+
+ This primitive allows to save a value to the checkpointer distinct from the
+ return value from the entrypoint.
+
+ Example: Decoupling the return value and the save value
+ ```python
+ from langgraph.checkpoint.memory import InMemorySaver
+ from langgraph.func import entrypoint
+
+
+ @entrypoint(checkpointer=InMemorySaver())
+ def my_workflow(
+ number: int,
+ *,
+ previous: Any = None,
+ ) -> entrypoint.final[int, int]:
+ previous = previous or 0
+ # This will return the previous value to the caller, saving
+ # 2 * number to the checkpoint, which will be used in the next invocation
+ # for the `previous` parameter.
+ return entrypoint.final(value=previous, save=2 * number)
+
+
+ config = {"configurable": {"thread_id": "1"}}
+
+ my_workflow.invoke(3, config) # 0 (previous was None)
+ my_workflow.invoke(1, config) # 6 (previous was 3 * 2 from the previous invocation)
+ ```
+ """
+
+ value: R
+ """Value to return. A value will always be returned even if it is `None`."""
+ save: S
+ """The value for the state for the next checkpoint.
+
+ A value will always be saved even if it is `None`.
+ """
+
+ def __call__(self, func: Callable[..., Any]) -> Pregel:
+ """Convert a function into a Pregel graph.
+
+ Args:
+ func: The function to convert. Support both sync and async functions.
+
+ Returns:
+ A Pregel graph.
+ """
+ # wrap generators in a function that writes to StreamWriter
+ if inspect.isgeneratorfunction(func) or inspect.isasyncgenfunction(func):
+ raise NotImplementedError(
+ "Generators are not supported in the Functional API."
+ )
+
+ bound = get_runnable_for_entrypoint(func)
+ stream_mode: StreamMode = "updates"
+
+ # get input and output types
+ sig = inspect.signature(func)
+ first_parameter_name = next(iter(sig.parameters.keys()), None)
+ if not first_parameter_name:
+ raise ValueError("Entrypoint function must have at least one parameter")
+ input_type = (
+ sig.parameters[first_parameter_name].annotation
+ if sig.parameters[first_parameter_name].annotation
+ is not inspect.Signature.empty
+ else Any
+ )
+
+ def _pluck_return_value(value: Any) -> Any:
+ """Extract the return_ value the entrypoint.final object or passthrough."""
+ return value.value if isinstance(value, entrypoint.final) else value
+
+ def _pluck_save_value(value: Any) -> Any:
+ """Get save value from the entrypoint.final object or passthrough."""
+ return value.save if isinstance(value, entrypoint.final) else value
+
+ output_type, save_type = Any, Any
+ if sig.return_annotation is not inspect.Signature.empty:
+ # User does not parameterize entrypoint.final properly
+ if (
+ sig.return_annotation is entrypoint.final
+ ): # Un-parameterized entrypoint.final
+ output_type = save_type = Any
+ else:
+ origin = get_origin(sig.return_annotation)
+ if origin is entrypoint.final:
+ type_annotations = get_args(sig.return_annotation)
+ if len(type_annotations) != 2:
+ raise TypeError(
+ "Please an annotation for both the return_ and "
+ "the save values."
+ "For example, `-> entrypoint.final[int, str]` would assign a "
+ "return_ a type of `int` and save the type `str`."
+ )
+ output_type, save_type = get_args(sig.return_annotation)
+ else:
+ output_type = save_type = sig.return_annotation
+
+ return Pregel(
+ nodes={
+ func.__name__: PregelNode(
+ bound=bound,
+ triggers=[START],
+ channels=START,
+ writers=[
+ ChannelWrite(
+ [
+ ChannelWriteEntry(END, mapper=_pluck_return_value),
+ ChannelWriteEntry(PREVIOUS, mapper=_pluck_save_value),
+ ]
+ )
+ ],
+ )
+ },
+ channels={
+ START: EphemeralValue(input_type),
+ END: LastValue(output_type, END),
+ PREVIOUS: LastValue(save_type, PREVIOUS),
+ },
+ input_channels=START,
+ output_channels=END,
+ stream_channels=END,
+ stream_mode=stream_mode,
+ stream_eager=True,
+ checkpointer=self.checkpointer,
+ store=self.store,
+ cache=self.cache,
+ cache_policy=self.cache_policy,
+ retry_policy=self.retry_policy or (),
+ context_schema=self.context_schema, # type: ignore[arg-type]
+ )
diff --git a/langgraph/source/libs/langgraph/langgraph/graph/__init__.py b/langgraph/source/libs/langgraph/langgraph/graph/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..7bea3fc8294fa3f0fc60451d46e40fa1d74c28fd
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/graph/__init__.py
@@ -0,0 +1,12 @@
+from langgraph.constants import END, START
+from langgraph.graph.message import MessageGraph, MessagesState, add_messages
+from langgraph.graph.state import StateGraph
+
+__all__ = (
+ "END",
+ "START",
+ "StateGraph",
+ "add_messages",
+ "MessagesState",
+ "MessageGraph",
+)
diff --git a/langgraph/source/libs/langgraph/langgraph/graph/_branch.py b/langgraph/source/libs/langgraph/langgraph/graph/_branch.py
new file mode 100644
index 0000000000000000000000000000000000000000..df5136d5f741742a9e25f9ea6315d0eefe744f07
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/graph/_branch.py
@@ -0,0 +1,225 @@
+from __future__ import annotations
+
+from collections.abc import Awaitable, Callable, Hashable, Sequence
+from inspect import (
+ isfunction,
+ ismethod,
+ signature,
+)
+from itertools import zip_longest
+from types import FunctionType
+from typing import (
+ Any,
+ Literal,
+ NamedTuple,
+ cast,
+ get_args,
+ get_origin,
+ get_type_hints,
+)
+
+from langchain_core.runnables import (
+ Runnable,
+ RunnableConfig,
+ RunnableLambda,
+)
+
+from langgraph._internal._runnable import (
+ RunnableCallable,
+)
+from langgraph.constants import END, START
+from langgraph.errors import InvalidUpdateError
+from langgraph.pregel._write import PASSTHROUGH, ChannelWrite, ChannelWriteEntry
+from langgraph.types import Send
+
+_Writer = Callable[
+ [Sequence[str | Send], bool],
+ Sequence[ChannelWriteEntry | Send],
+]
+
+
+def _get_branch_path_input_schema(
+ path: Callable[..., Hashable | Sequence[Hashable]]
+ | Callable[..., Awaitable[Hashable | Sequence[Hashable]]]
+ | Runnable[Any, Hashable | Sequence[Hashable]],
+) -> type[Any] | None:
+ input = None
+ # detect input schema annotation in the branch callable
+ try:
+ callable_: (
+ Callable[..., Hashable | Sequence[Hashable]]
+ | Callable[..., Awaitable[Hashable | Sequence[Hashable]]]
+ | None
+ ) = None
+ if isinstance(path, (RunnableCallable, RunnableLambda)):
+ if isfunction(path.func) or ismethod(path.func):
+ callable_ = path.func
+ elif (callable_method := getattr(path.func, "__call__", None)) and ismethod(
+ callable_method
+ ):
+ callable_ = callable_method
+ elif isfunction(path.afunc) or ismethod(path.afunc):
+ callable_ = path.afunc
+ elif (
+ callable_method := getattr(path.afunc, "__call__", None)
+ ) and ismethod(callable_method):
+ callable_ = callable_method
+ elif callable(path):
+ callable_ = path
+
+ if callable_ is not None and (hints := get_type_hints(callable_)):
+ first_parameter_name = next(
+ iter(signature(cast(FunctionType, callable_)).parameters.keys())
+ )
+ if input_hint := hints.get(first_parameter_name):
+ if isinstance(input_hint, type) and get_type_hints(input_hint):
+ input = input_hint
+ except (TypeError, StopIteration):
+ pass
+
+ return input
+
+
+class BranchSpec(NamedTuple):
+ path: Runnable[Any, Hashable | list[Hashable]]
+ ends: dict[Hashable, str] | None
+ input_schema: type[Any] | None = None
+
+ @classmethod
+ def from_path(
+ cls,
+ path: Runnable[Any, Hashable | list[Hashable]],
+ path_map: dict[Hashable, str] | list[str] | None,
+ infer_schema: bool = False,
+ ) -> BranchSpec:
+ # coerce path_map to a dictionary
+ path_map_: dict[Hashable, str] | None = None
+ try:
+ if isinstance(path_map, dict):
+ path_map_ = path_map.copy()
+ elif isinstance(path_map, list):
+ path_map_ = {name: name for name in path_map}
+ else:
+ # find func
+ func: Callable | None = None
+ if isinstance(path, (RunnableCallable, RunnableLambda)):
+ func = path.func or path.afunc
+ if func is not None:
+ # find callable method
+ if (cal := getattr(path, "__call__", None)) and ismethod(cal):
+ func = cal
+ # get the return type
+ if rtn_type := get_type_hints(func).get("return"):
+ if get_origin(rtn_type) is Literal:
+ path_map_ = {name: name for name in get_args(rtn_type)}
+ except Exception:
+ pass
+ # infer input schema
+ input_schema = _get_branch_path_input_schema(path) if infer_schema else None
+ # create branch
+ return cls(path=path, ends=path_map_, input_schema=input_schema)
+
+ def run(
+ self,
+ writer: _Writer,
+ reader: Callable[[RunnableConfig], Any] | None = None,
+ ) -> RunnableCallable:
+ return ChannelWrite.register_writer(
+ RunnableCallable(
+ func=self._route,
+ afunc=self._aroute,
+ writer=writer,
+ reader=reader,
+ name=None,
+ trace=False,
+ ),
+ list(
+ zip_longest(
+ writer([e for e in self.ends.values()], True),
+ [str(la) for la, e in self.ends.items()],
+ )
+ )
+ if self.ends
+ else None,
+ )
+
+ def _route(
+ self,
+ input: Any,
+ config: RunnableConfig,
+ *,
+ reader: Callable[[RunnableConfig], Any] | None,
+ writer: _Writer,
+ ) -> Runnable:
+ if reader:
+ value = reader(config)
+ # passthrough additional keys from node to branch
+ # only doable when using dict states
+ if (
+ isinstance(value, dict)
+ and isinstance(input, dict)
+ and self.input_schema is None
+ ):
+ value = {**input, **value}
+ else:
+ value = input
+ result = self.path.invoke(value, config)
+ return self._finish(writer, input, result, config)
+
+ async def _aroute(
+ self,
+ input: Any,
+ config: RunnableConfig,
+ *,
+ reader: Callable[[RunnableConfig], Any] | None,
+ writer: _Writer,
+ ) -> Runnable:
+ if reader:
+ value = reader(config)
+ # passthrough additional keys from node to branch
+ # only doable when using dict states
+ if (
+ isinstance(value, dict)
+ and isinstance(input, dict)
+ and self.input_schema is None
+ ):
+ value = {**input, **value}
+ else:
+ value = input
+ result = await self.path.ainvoke(value, config)
+ return self._finish(writer, input, result, config)
+
+ def _finish(
+ self,
+ writer: _Writer,
+ input: Any,
+ result: Any,
+ config: RunnableConfig,
+ ) -> Runnable | Any:
+ if not isinstance(result, (list, tuple)):
+ result = [result]
+ if self.ends:
+ destinations: Sequence[Send | str] = [
+ r if isinstance(r, Send) else self.ends[r] for r in result
+ ]
+ else:
+ destinations = cast(Sequence[Send | str], result)
+ if any(dest is None or dest == START for dest in destinations):
+ raise ValueError("Branch did not return a valid destination")
+ if any(p.node == END for p in destinations if isinstance(p, Send)):
+ raise InvalidUpdateError("Cannot send a packet to the END node")
+ entries = writer(destinations, False)
+ if not entries:
+ return input
+ else:
+ need_passthrough = False
+ for e in entries:
+ if isinstance(e, ChannelWriteEntry):
+ if e.value is PASSTHROUGH:
+ need_passthrough = True
+ break
+ if need_passthrough:
+ return ChannelWrite(entries)
+ else:
+ ChannelWrite.do_write(config, entries)
+ return input
diff --git a/langgraph/source/libs/langgraph/langgraph/graph/_node.py b/langgraph/source/libs/langgraph/langgraph/graph/_node.py
new file mode 100644
index 0000000000000000000000000000000000000000..cadf097d9b13401815168221c4613ad9d6f54857
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/graph/_node.py
@@ -0,0 +1,92 @@
+from __future__ import annotations
+
+from collections.abc import Sequence
+from dataclasses import dataclass
+from typing import Any, Generic, Protocol, TypeAlias
+
+from langchain_core.runnables import Runnable, RunnableConfig
+from langgraph.store.base import BaseStore
+
+from langgraph._internal._typing import EMPTY_SEQ
+from langgraph.runtime import Runtime
+from langgraph.types import CachePolicy, RetryPolicy, StreamWriter
+from langgraph.typing import ContextT, NodeInputT, NodeInputT_contra
+
+
+class _Node(Protocol[NodeInputT_contra]):
+ def __call__(self, state: NodeInputT_contra) -> Any: ...
+
+
+class _NodeWithConfig(Protocol[NodeInputT_contra]):
+ def __call__(self, state: NodeInputT_contra, config: RunnableConfig) -> Any: ...
+
+
+class _NodeWithWriter(Protocol[NodeInputT_contra]):
+ def __call__(self, state: NodeInputT_contra, *, writer: StreamWriter) -> Any: ...
+
+
+class _NodeWithStore(Protocol[NodeInputT_contra]):
+ def __call__(self, state: NodeInputT_contra, *, store: BaseStore) -> Any: ...
+
+
+class _NodeWithWriterStore(Protocol[NodeInputT_contra]):
+ def __call__(
+ self, state: NodeInputT_contra, *, writer: StreamWriter, store: BaseStore
+ ) -> Any: ...
+
+
+class _NodeWithConfigWriter(Protocol[NodeInputT_contra]):
+ def __call__(
+ self, state: NodeInputT_contra, *, config: RunnableConfig, writer: StreamWriter
+ ) -> Any: ...
+
+
+class _NodeWithConfigStore(Protocol[NodeInputT_contra]):
+ def __call__(
+ self, state: NodeInputT_contra, *, config: RunnableConfig, store: BaseStore
+ ) -> Any: ...
+
+
+class _NodeWithConfigWriterStore(Protocol[NodeInputT_contra]):
+ def __call__(
+ self,
+ state: NodeInputT_contra,
+ *,
+ config: RunnableConfig,
+ writer: StreamWriter,
+ store: BaseStore,
+ ) -> Any: ...
+
+
+class _NodeWithRuntime(Protocol[NodeInputT_contra, ContextT]):
+ def __call__(
+ self, state: NodeInputT_contra, *, runtime: Runtime[ContextT]
+ ) -> Any: ...
+
+
+# TODO: we probably don't want to explicitly support the config / store signatures once
+# we move to adding a context arg. Maybe what we do is we add support for kwargs with param spec
+# this is purely for typing purposes though, so can easily change in the coming weeks.
+StateNode: TypeAlias = (
+ _Node[NodeInputT]
+ | _NodeWithConfig[NodeInputT]
+ | _NodeWithWriter[NodeInputT]
+ | _NodeWithStore[NodeInputT]
+ | _NodeWithWriterStore[NodeInputT]
+ | _NodeWithConfigWriter[NodeInputT]
+ | _NodeWithConfigStore[NodeInputT]
+ | _NodeWithConfigWriterStore[NodeInputT]
+ | _NodeWithRuntime[NodeInputT, ContextT]
+ | Runnable[NodeInputT, Any]
+)
+
+
+@dataclass(slots=True)
+class StateNodeSpec(Generic[NodeInputT, ContextT]):
+ runnable: StateNode[NodeInputT, ContextT]
+ metadata: dict[str, Any] | None
+ input_schema: type[NodeInputT]
+ retry_policy: RetryPolicy | Sequence[RetryPolicy] | None
+ cache_policy: CachePolicy | None
+ ends: tuple[str, ...] | dict[str, str] | None = EMPTY_SEQ
+ defer: bool = False
diff --git a/langgraph/source/libs/langgraph/langgraph/graph/message.py b/langgraph/source/libs/langgraph/langgraph/graph/message.py
new file mode 100644
index 0000000000000000000000000000000000000000..18c96d436cac0cf6f35d8771836a391a25d0da07
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/graph/message.py
@@ -0,0 +1,372 @@
+from __future__ import annotations
+
+import uuid
+import warnings
+from collections.abc import Callable, Sequence
+from functools import partial
+from typing import (
+ Annotated,
+ Any,
+ Literal,
+ cast,
+)
+
+from langchain_core.messages import (
+ AnyMessage,
+ BaseMessage,
+ BaseMessageChunk,
+ MessageLikeRepresentation,
+ RemoveMessage,
+ convert_to_messages,
+ message_chunk_to_message,
+)
+from typing_extensions import TypedDict, deprecated
+
+from langgraph._internal._constants import CONF, CONFIG_KEY_SEND, NS_SEP
+from langgraph.graph.state import StateGraph
+from langgraph.warnings import LangGraphDeprecatedSinceV10
+
+__all__ = (
+ "add_messages",
+ "MessagesState",
+ "MessageGraph",
+ "REMOVE_ALL_MESSAGES",
+)
+
+Messages = list[MessageLikeRepresentation] | MessageLikeRepresentation
+
+REMOVE_ALL_MESSAGES = "__remove_all__"
+
+
+def _add_messages_wrapper(func: Callable) -> Callable[[Messages, Messages], Messages]:
+ def _add_messages(
+ left: Messages | None = None, right: Messages | None = None, **kwargs: Any
+ ) -> Messages | Callable[[Messages, Messages], Messages]:
+ if left is not None and right is not None:
+ return func(left, right, **kwargs)
+ elif left is not None or right is not None:
+ msg = (
+ f"Must specify non-null arguments for both 'left' and 'right'. Only "
+ f"received: '{'left' if left else 'right'}'."
+ )
+ raise ValueError(msg)
+ else:
+ return partial(func, **kwargs)
+
+ _add_messages.__doc__ = func.__doc__
+ return cast(Callable[[Messages, Messages], Messages], _add_messages)
+
+
+@_add_messages_wrapper
+def add_messages(
+ left: Messages,
+ right: Messages,
+ *,
+ format: Literal["langchain-openai"] | None = None,
+) -> Messages:
+ """Merges two lists of messages, updating existing messages by ID.
+
+ By default, this ensures the state is "append-only", unless the
+ new message has the same ID as an existing message.
+
+ Args:
+ left: The base list of `Messages`.
+ right: The list of `Messages` (or single `Message`) to merge
+ into the base list.
+ format: The format to return messages in. If `None` then `Messages` will be
+ returned as is. If `langchain-openai` then `Messages` will be returned as
+ `BaseMessage` objects with their contents formatted to match OpenAI message
+ format, meaning contents can be string, `'text'` blocks, or `'image_url'` blocks
+ and tool responses are returned as their own `ToolMessage` objects.
+
+ !!! important "Requirement"
+
+ Must have `langchain-core>=0.3.11` installed to use this feature.
+
+ Returns:
+ A new list of messages with the messages from `right` merged into `left`.
+ If a message in `right` has the same ID as a message in `left`, the
+ message from `right` will replace the message from `left`.
+
+ Example: Basic usage
+ ```python
+ from langchain_core.messages import AIMessage, HumanMessage
+
+ msgs1 = [HumanMessage(content="Hello", id="1")]
+ msgs2 = [AIMessage(content="Hi there!", id="2")]
+ add_messages(msgs1, msgs2)
+ # [HumanMessage(content='Hello', id='1'), AIMessage(content='Hi there!', id='2')]
+ ```
+
+ Example: Overwrite existing message
+ ```python
+ msgs1 = [HumanMessage(content="Hello", id="1")]
+ msgs2 = [HumanMessage(content="Hello again", id="1")]
+ add_messages(msgs1, msgs2)
+ # [HumanMessage(content='Hello again', id='1')]
+ ```
+
+ Example: Use in a StateGraph
+ ```python
+ from typing import Annotated
+ from typing_extensions import TypedDict
+ from langgraph.graph import StateGraph
+
+
+ class State(TypedDict):
+ messages: Annotated[list, add_messages]
+
+
+ builder = StateGraph(State)
+ builder.add_node("chatbot", lambda state: {"messages": [("assistant", "Hello")]})
+ builder.set_entry_point("chatbot")
+ builder.set_finish_point("chatbot")
+ graph = builder.compile()
+ graph.invoke({})
+ # {'messages': [AIMessage(content='Hello', id=...)]}
+ ```
+
+ Example: Use OpenAI message format
+ ```python
+ from typing import Annotated
+ from typing_extensions import TypedDict
+ from langgraph.graph import StateGraph, add_messages
+
+
+ class State(TypedDict):
+ messages: Annotated[list, add_messages(format="langchain-openai")]
+
+
+ def chatbot_node(state: State) -> list:
+ return {
+ "messages": [
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "text",
+ "text": "Here's an image:",
+ "cache_control": {"type": "ephemeral"},
+ },
+ {
+ "type": "image",
+ "source": {
+ "type": "base64",
+ "media_type": "image/jpeg",
+ "data": "1234",
+ },
+ },
+ ],
+ },
+ ]
+ }
+
+
+ builder = StateGraph(State)
+ builder.add_node("chatbot", chatbot_node)
+ builder.set_entry_point("chatbot")
+ builder.set_finish_point("chatbot")
+ graph = builder.compile()
+ graph.invoke({"messages": []})
+ # {
+ # 'messages': [
+ # HumanMessage(
+ # content=[
+ # {"type": "text", "text": "Here's an image:"},
+ # {
+ # "type": "image_url",
+ # "image_url": {"url": "data:image/jpeg;base64,1234"},
+ # },
+ # ],
+ # ),
+ # ]
+ # }
+ ```
+
+ """
+ remove_all_idx = None
+ # coerce to list
+ if not isinstance(left, list):
+ left = [left] # type: ignore[assignment]
+ if not isinstance(right, list):
+ right = [right] # type: ignore[assignment]
+ # coerce to message
+ left = [
+ message_chunk_to_message(cast(BaseMessageChunk, m))
+ for m in convert_to_messages(left)
+ ]
+ right = [
+ message_chunk_to_message(cast(BaseMessageChunk, m))
+ for m in convert_to_messages(right)
+ ]
+ # assign missing ids
+ for m in left:
+ if m.id is None:
+ m.id = str(uuid.uuid4())
+ for idx, m in enumerate(right):
+ if m.id is None:
+ m.id = str(uuid.uuid4())
+ if isinstance(m, RemoveMessage) and m.id == REMOVE_ALL_MESSAGES:
+ remove_all_idx = idx
+
+ if remove_all_idx is not None:
+ return right[remove_all_idx + 1 :]
+
+ # merge
+ merged = left.copy()
+ merged_by_id = {m.id: i for i, m in enumerate(merged)}
+ ids_to_remove = set()
+ for m in right:
+ if (existing_idx := merged_by_id.get(m.id)) is not None:
+ if isinstance(m, RemoveMessage):
+ ids_to_remove.add(m.id)
+ else:
+ ids_to_remove.discard(m.id)
+ merged[existing_idx] = m
+ else:
+ if isinstance(m, RemoveMessage):
+ raise ValueError(
+ f"Attempting to delete a message with an ID that doesn't exist ('{m.id}')"
+ )
+
+ merged_by_id[m.id] = len(merged)
+ merged.append(m)
+ merged = [m for m in merged if m.id not in ids_to_remove]
+
+ if format == "langchain-openai":
+ merged = _format_messages(merged)
+ elif format:
+ msg = f"Unrecognized {format=}. Expected one of 'langchain-openai', None."
+ raise ValueError(msg)
+ else:
+ pass
+
+ return merged
+
+
+@deprecated(
+ "MessageGraph is deprecated in langgraph 1.0.0, to be removed in 2.0.0. Please use StateGraph with a `messages` key instead.",
+ category=None,
+)
+class MessageGraph(StateGraph):
+ """A StateGraph where every node receives a list of messages as input and returns one or more messages as output.
+
+ MessageGraph is a subclass of StateGraph whose entire state is a single, append-only* list of messages.
+ Each node in a MessageGraph takes a list of messages as input and returns zero or more
+ messages as output. The `add_messages` function is used to merge the output messages from each node
+ into the existing list of messages in the graph's state.
+
+ Examples:
+ ```pycon
+ >>> from langgraph.graph.message import MessageGraph
+ ...
+ >>> builder = MessageGraph()
+ >>> builder.add_node("chatbot", lambda state: [("assistant", "Hello!")])
+ >>> builder.set_entry_point("chatbot")
+ >>> builder.set_finish_point("chatbot")
+ >>> builder.compile().invoke([("user", "Hi there.")])
+ [HumanMessage(content="Hi there.", id='...'), AIMessage(content="Hello!", id='...')]
+ ```
+
+ ```pycon
+ >>> from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
+ >>> from langgraph.graph.message import MessageGraph
+ ...
+ >>> builder = MessageGraph()
+ >>> builder.add_node(
+ ... "chatbot",
+ ... lambda state: [
+ ... AIMessage(
+ ... content="Hello!",
+ ... tool_calls=[{"name": "search", "id": "123", "args": {"query": "X"}}],
+ ... )
+ ... ],
+ ... )
+ >>> builder.add_node(
+ ... "search", lambda state: [ToolMessage(content="Searching...", tool_call_id="123")]
+ ... )
+ >>> builder.set_entry_point("chatbot")
+ >>> builder.add_edge("chatbot", "search")
+ >>> builder.set_finish_point("search")
+ >>> builder.compile().invoke([HumanMessage(content="Hi there. Can you search for X?")])
+ {'messages': [HumanMessage(content="Hi there. Can you search for X?", id='b8b7d8f4-7f4d-4f4d-9c1d-f8b8d8f4d9c1'),
+ AIMessage(content="Hello!", id='f4d9c1d8-8d8f-4d9c-b8b7-d8f4f4d9c1d8'),
+ ToolMessage(content="Searching...", id='d8f4f4d9-c1d8-4f4d-b8b7-d8f4f4d9c1d8', tool_call_id="123")]}
+ ```
+ """
+
+ def __init__(self) -> None:
+ warnings.warn(
+ "MessageGraph is deprecated in LangGraph v1.0.0, to be removed in v2.0.0. Please use StateGraph with a `messages` key instead.",
+ category=LangGraphDeprecatedSinceV10,
+ stacklevel=2,
+ )
+ super().__init__(Annotated[list[AnyMessage], add_messages]) # type: ignore[arg-type]
+
+
+class MessagesState(TypedDict):
+ messages: Annotated[list[AnyMessage], add_messages]
+
+
+def _format_messages(messages: Sequence[BaseMessage]) -> list[BaseMessage]:
+ try:
+ from langchain_core.messages import convert_to_openai_messages
+ except ImportError:
+ msg = (
+ "Must have langchain-core>=0.3.11 installed to use automatic message "
+ "formatting (format='langchain-openai'). Please update your langchain-core "
+ "version or remove the 'format' flag. Returning un-formatted "
+ "messages."
+ )
+ warnings.warn(msg)
+ return list(messages)
+ else:
+ return convert_to_messages(convert_to_openai_messages(messages))
+
+
+def push_message(
+ message: MessageLikeRepresentation | BaseMessageChunk,
+ *,
+ state_key: str | None = "messages",
+) -> AnyMessage:
+ """Write a message manually to the `messages` / `messages-tuple` stream mode.
+
+ Will automatically write to the channel specified in the `state_key` unless `state_key` is `None`.
+ """
+
+ from langchain_core.callbacks.base import (
+ BaseCallbackHandler,
+ BaseCallbackManager,
+ )
+
+ from langgraph.config import get_config
+ from langgraph.pregel._messages import StreamMessagesHandler
+
+ config = get_config()
+ message = next(x for x in convert_to_messages([message]))
+
+ if message.id is None:
+ raise ValueError("Message ID is required")
+
+ if isinstance(config["callbacks"], BaseCallbackManager):
+ manager = config["callbacks"]
+ handlers = manager.handlers
+ elif isinstance(config["callbacks"], list) and all(
+ isinstance(x, BaseCallbackHandler) for x in config["callbacks"]
+ ):
+ handlers = config["callbacks"]
+
+ if stream_handler := next(
+ (x for x in handlers if isinstance(x, StreamMessagesHandler)), None
+ ):
+ metadata = config["metadata"]
+ message_meta = (
+ tuple(cast(str, metadata["langgraph_checkpoint_ns"]).split(NS_SEP)),
+ metadata,
+ )
+ stream_handler._emit(message_meta, message, dedupe=False)
+
+ if state_key:
+ config[CONF][CONFIG_KEY_SEND]([(state_key, message)])
+
+ return message
diff --git a/langgraph/source/libs/langgraph/langgraph/graph/state.py b/langgraph/source/libs/langgraph/langgraph/graph/state.py
new file mode 100644
index 0000000000000000000000000000000000000000..4d0c90457d9da38a5f1aef1c5ae6bc1eab3a2b6b
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/graph/state.py
@@ -0,0 +1,1707 @@
+from __future__ import annotations
+
+import inspect
+import logging
+import typing
+import warnings
+from collections import defaultdict
+from collections.abc import Awaitable, Callable, Hashable, Sequence
+from functools import partial
+from inspect import isclass, isfunction, ismethod, signature
+from types import FunctionType
+from types import NoneType as NoneType
+from typing import (
+ Any,
+ Generic,
+ Literal,
+ Union,
+ cast,
+ get_args,
+ get_origin,
+ get_type_hints,
+ overload,
+)
+
+from langchain_core.runnables import Runnable, RunnableConfig
+from langgraph.cache.base import BaseCache
+from langgraph.checkpoint.base import Checkpoint
+from langgraph.store.base import BaseStore
+from pydantic import BaseModel, TypeAdapter
+from typing_extensions import NotRequired, Required, Self, Unpack, is_typeddict
+
+from langgraph._internal._constants import (
+ INTERRUPT,
+ NS_END,
+ NS_SEP,
+ TASKS,
+)
+from langgraph._internal._fields import (
+ get_cached_annotated_keys,
+ get_field_default,
+ get_update_as_tuples,
+)
+from langgraph._internal._pydantic import create_model
+from langgraph._internal._runnable import coerce_to_runnable
+from langgraph._internal._typing import EMPTY_SEQ, MISSING, DeprecatedKwargs
+from langgraph.channels.base import BaseChannel
+from langgraph.channels.binop import BinaryOperatorAggregate
+from langgraph.channels.ephemeral_value import EphemeralValue
+from langgraph.channels.last_value import LastValue, LastValueAfterFinish
+from langgraph.channels.named_barrier_value import (
+ NamedBarrierValue,
+ NamedBarrierValueAfterFinish,
+)
+from langgraph.constants import END, START, TAG_HIDDEN
+from langgraph.errors import (
+ ErrorCode,
+ InvalidUpdateError,
+ ParentCommand,
+ create_error_message,
+)
+from langgraph.graph._branch import BranchSpec
+from langgraph.graph._node import StateNode, StateNodeSpec
+from langgraph.managed.base import (
+ ManagedValueSpec,
+ is_managed_value,
+)
+from langgraph.pregel import Pregel
+from langgraph.pregel._read import ChannelRead, PregelNode
+from langgraph.pregel._write import (
+ ChannelWrite,
+ ChannelWriteEntry,
+ ChannelWriteTupleEntry,
+)
+from langgraph.types import (
+ All,
+ CachePolicy,
+ Checkpointer,
+ Command,
+ RetryPolicy,
+ Send,
+ ensure_valid_checkpointer,
+)
+from langgraph.typing import ContextT, InputT, NodeInputT, OutputT, StateT
+from langgraph.warnings import LangGraphDeprecatedSinceV05, LangGraphDeprecatedSinceV10
+
+__all__ = ("StateGraph", "CompiledStateGraph")
+
+logger = logging.getLogger(__name__)
+
+_CHANNEL_BRANCH_TO = "branch:to:{}"
+
+
+def _warn_invalid_state_schema(schema: type[Any] | Any) -> None:
+ if isinstance(schema, type):
+ return
+ if typing.get_args(schema):
+ return
+ warnings.warn(
+ f"Invalid state_schema: {schema}. Expected a type or Annotated[type, reducer]. "
+ "Please provide a valid schema to ensure correct updates.\n"
+ " See: https://langchain-ai.github.io/langgraph/reference/graphs/#stategraph"
+ )
+
+
+def _get_node_name(node: StateNode[Any, ContextT]) -> str:
+ try:
+ return getattr(node, "__name__", node.__class__.__name__)
+ except AttributeError:
+ raise TypeError(f"Unsupported node type: {type(node)}")
+
+
+class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
+ """A graph whose nodes communicate by reading and writing to a shared state.
+
+ The signature of each node is `State -> Partial`.
+
+ Each state key can optionally be annotated with a reducer function that
+ will be used to aggregate the values of that key received from multiple nodes.
+ The signature of a reducer function is `(Value, Value) -> Value`.
+
+ !!! warning
+
+ `StateGraph` is a builder class and cannot be used directly for execution.
+ You must first call `.compile()` to create an executable graph that supports
+ methods like `invoke()`, `stream()`, `astream()`, and `ainvoke()`. See the
+ `CompiledStateGraph` documentation for more details.
+
+ Args:
+ state_schema: The schema class that defines the state.
+ context_schema: The schema class that defines the runtime context.
+
+ Use this to expose immutable context data to your nodes, like `user_id`, `db_conn`, etc.
+ input_schema: The schema class that defines the input to the graph.
+ output_schema: The schema class that defines the output from the graph.
+
+ !!! warning "`config_schema` Deprecated"
+ The `config_schema` parameter is deprecated in v0.6.0 and support will be removed in v2.0.0.
+ Please use `context_schema` instead to specify the schema for run-scoped context.
+
+ Example:
+ ```python
+ from langchain_core.runnables import RunnableConfig
+ from typing_extensions import Annotated, TypedDict
+ from langgraph.checkpoint.memory import InMemorySaver
+ from langgraph.graph import StateGraph
+ from langgraph.runtime import Runtime
+
+
+ def reducer(a: list, b: int | None) -> list:
+ if b is not None:
+ return a + [b]
+ return a
+
+
+ class State(TypedDict):
+ x: Annotated[list, reducer]
+
+
+ class Context(TypedDict):
+ r: float
+
+
+ graph = StateGraph(state_schema=State, context_schema=Context)
+
+
+ def node(state: State, runtime: Runtime[Context]) -> dict:
+ r = runtime.context.get("r", 1.0)
+ x = state["x"][-1]
+ next_value = x * r * (1 - x)
+ return {"x": next_value}
+
+
+ graph.add_node("A", node)
+ graph.set_entry_point("A")
+ graph.set_finish_point("A")
+ compiled = graph.compile()
+
+ step1 = compiled.invoke({"x": 0.5}, context={"r": 3.0})
+ # {'x': [0.5, 0.75]}
+ ```
+ """
+
+ edges: set[tuple[str, str]]
+ nodes: dict[str, StateNodeSpec[Any, ContextT]]
+ branches: defaultdict[str, dict[str, BranchSpec]]
+ channels: dict[str, BaseChannel]
+ managed: dict[str, ManagedValueSpec]
+ schemas: dict[type[Any], dict[str, BaseChannel | ManagedValueSpec]]
+ waiting_edges: set[tuple[tuple[str, ...], str]]
+
+ compiled: bool
+ state_schema: type[StateT]
+ context_schema: type[ContextT] | None
+ input_schema: type[InputT]
+ output_schema: type[OutputT]
+
+ def __init__(
+ self,
+ state_schema: type[StateT],
+ context_schema: type[ContextT] | None = None,
+ *,
+ input_schema: type[InputT] | None = None,
+ output_schema: type[OutputT] | None = None,
+ **kwargs: Unpack[DeprecatedKwargs],
+ ) -> None:
+ if (config_schema := kwargs.get("config_schema", MISSING)) is not MISSING:
+ warnings.warn(
+ "`config_schema` is deprecated and will be removed. Please use `context_schema` instead.",
+ category=LangGraphDeprecatedSinceV10,
+ stacklevel=2,
+ )
+ if context_schema is None:
+ context_schema = cast(type[ContextT], config_schema)
+
+ if (input_ := kwargs.get("input", MISSING)) is not MISSING:
+ warnings.warn(
+ "`input` is deprecated and will be removed. Please use `input_schema` instead.",
+ category=LangGraphDeprecatedSinceV05,
+ stacklevel=2,
+ )
+ if input_schema is None:
+ input_schema = cast(type[InputT], input_)
+
+ if (output := kwargs.get("output", MISSING)) is not MISSING:
+ warnings.warn(
+ "`output` is deprecated and will be removed. Please use `output_schema` instead.",
+ category=LangGraphDeprecatedSinceV05,
+ stacklevel=2,
+ )
+ if output_schema is None:
+ output_schema = cast(type[OutputT], output)
+
+ self.nodes = {}
+ self.edges = set()
+ self.branches = defaultdict(dict)
+ self.schemas = {}
+ self.channels = {}
+ self.managed = {}
+ self.compiled = False
+ self.waiting_edges = set()
+
+ self.state_schema = state_schema
+ self.input_schema = cast(type[InputT], input_schema or state_schema)
+ self.output_schema = cast(type[OutputT], output_schema or state_schema)
+ self.context_schema = context_schema
+
+ self._add_schema(self.state_schema)
+ self._add_schema(self.input_schema, allow_managed=False)
+ self._add_schema(self.output_schema, allow_managed=False)
+
+ @property
+ def _all_edges(self) -> set[tuple[str, str]]:
+ return self.edges | {
+ (start, end) for starts, end in self.waiting_edges for start in starts
+ }
+
+ def _add_schema(self, schema: type[Any], /, allow_managed: bool = True) -> None:
+ if schema not in self.schemas:
+ _warn_invalid_state_schema(schema)
+ channels, managed, type_hints = _get_channels(schema)
+ if managed and not allow_managed:
+ names = ", ".join(managed)
+ schema_name = getattr(schema, "__name__", "")
+ raise ValueError(
+ f"Invalid managed channels detected in {schema_name}: {names}."
+ " Managed channels are not permitted in Input/Output schema."
+ )
+ self.schemas[schema] = {**channels, **managed}
+ for key, channel in channels.items():
+ if key in self.channels:
+ if self.channels[key] != channel:
+ if isinstance(channel, LastValue):
+ pass
+ else:
+ raise ValueError(
+ f"Channel '{key}' already exists with a different type"
+ )
+ else:
+ self.channels[key] = channel
+ for key, managed in managed.items():
+ if key in self.managed:
+ if self.managed[key] != managed:
+ raise ValueError(
+ f"Managed value '{key}' already exists with a different type"
+ )
+ else:
+ self.managed[key] = managed
+
+ @overload
+ def add_node(
+ self,
+ node: StateNode[NodeInputT, ContextT],
+ *,
+ defer: bool = False,
+ metadata: dict[str, Any] | None = None,
+ input_schema: None = None,
+ retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
+ cache_policy: CachePolicy | None = None,
+ destinations: dict[str, str] | tuple[str, ...] | None = None,
+ **kwargs: Unpack[DeprecatedKwargs],
+ ) -> Self:
+ """Add a new node to the `StateGraph`, input schema is inferred as the state schema.
+
+ Will take the name of the function/runnable as the node name.
+
+ Args:
+ node: The function or runnable this node will run.
+ defer: Whether to defer the execution of the node until the run is about to end.
+ metadata: The metadata associated with the node.
+ input_schema: The input schema for the node. (Default: the graph's state schema)
+ retry_policy: The retry policy for the node.
+
+ If a sequence is provided, the first matching policy will be applied.
+ cache_policy: The cache policy for the node.
+ destinations: Destinations that indicate where a node can route to.
+
+ Useful for edgeless graphs with nodes that return `Command` objects.
+
+ If a `dict` is provided, the keys will be used as the target node names and the values will be used as the labels for the edges.
+
+ If a `tuple` is provided, the values will be used as the target node names.
+
+ !!! warning
+
+ This is only used for graph rendering and doesn't have any effect on the graph execution.
+
+ Example:
+ ```python
+ from typing_extensions import TypedDict
+
+ from langchain_core.runnables import RunnableConfig
+ from langgraph.graph import START, StateGraph
+
+
+ class State(TypedDict):
+ x: int
+
+
+ def my_node(state: State, config: RunnableConfig) -> State:
+ return {"x": state["x"] + 1}
+
+
+ builder = StateGraph(State)
+ builder.add_node(my_node) # node name will be 'my_node'
+ builder.add_edge(START, "my_node")
+ graph = builder.compile()
+ graph.invoke({"x": 1})
+ # {'x': 2}
+ ```
+
+ Returns:
+ Self: The instance of the `StateGraph`, allowing for method chaining.
+ """
+ ...
+
+ @overload
+ def add_node(
+ self,
+ node: StateNode[NodeInputT, ContextT],
+ *,
+ defer: bool = False,
+ metadata: dict[str, Any] | None = None,
+ input_schema: type[NodeInputT],
+ retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
+ cache_policy: CachePolicy | None = None,
+ destinations: dict[str, str] | tuple[str, ...] | None = None,
+ **kwargs: Unpack[DeprecatedKwargs],
+ ) -> Self:
+ """Add a new node to the `StateGraph` where input schema is specified.
+
+ Will take the name of the function/runnable as the node name.
+
+ Args:
+ node: The function or runnable this node will run.
+ defer: Whether to defer the execution of the node until the run is about to end.
+ metadata: The metadata associated with the node.
+ input_schema: The input schema for the node.
+ retry_policy: The retry policy for the node.
+
+ If a sequence is provided, the first matching policy will be applied.
+ cache_policy: The cache policy for the node.
+ destinations: Destinations that indicate where a node can route to.
+
+ Useful for edgeless graphs with nodes that return `Command` objects.
+
+ If a `dict` is provided, the keys will be used as the target node names and the values will be used as the labels for the edges.
+
+ If a `tuple` is provided, the values will be used as the target node names.
+
+ !!! warning
+
+ This is only used for graph rendering and doesn't have any effect on the graph execution.
+
+ Example:
+ ```python
+ from typing_extensions import TypedDict
+
+ from langchain_core.runnables import RunnableConfig
+ from langgraph.graph import START, StateGraph
+
+
+ class State(TypedDict):
+ x: int
+
+
+ class NodeInput(TypedDict):
+ x: int
+
+
+ def my_node(state: NodeInput, config: RunnableConfig) -> State:
+ return {"x": state["x"] + 1}
+
+
+ builder = StateGraph(State)
+ builder.add_node(my_node, input_schema=NodeInput) # node name will be 'my_node'
+ builder.add_edge(START, "my_node")
+ graph = builder.compile()
+ graph.invoke({"x": 1})
+ # {'x': 2}
+ ```
+
+ Returns:
+ Self: The instance of the `StateGraph`, allowing for method chaining.
+ """
+ ...
+
+ @overload
+ def add_node(
+ self,
+ node: str,
+ action: StateNode[NodeInputT, ContextT],
+ *,
+ defer: bool = False,
+ metadata: dict[str, Any] | None = None,
+ input_schema: None = None,
+ retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
+ cache_policy: CachePolicy | None = None,
+ destinations: dict[str, str] | tuple[str, ...] | None = None,
+ **kwargs: Unpack[DeprecatedKwargs],
+ ) -> Self:
+ """Add a new node to the `StateGraph`, input schema is inferred as the state schema.
+
+ Args:
+ node: The name of the node.
+ action: The function or runnable this node will run.
+ defer: Whether to defer the execution of the node until the run is about to end.
+ metadata: The metadata associated with the node.
+ input_schema: The input schema for the node. (Default: the graph's state schema)
+ retry_policy: The retry policy for the node.
+
+ If a sequence is provided, the first matching policy will be applied.
+ cache_policy: The cache policy for the node.
+ destinations: Destinations that indicate where a node can route to.
+
+ Useful for edgeless graphs with nodes that return `Command` objects.
+
+ If a `dict` is provided, the keys will be used as the target node names and the values will be used as the labels for the edges.
+
+ If a `tuple` is provided, the values will be used as the target node names.
+
+ !!! warning
+
+ This is only used for graph rendering and doesn't have any effect on the graph execution.
+
+ Example:
+ ```python
+ from typing_extensions import TypedDict
+
+ from langchain_core.runnables import RunnableConfig
+ from langgraph.graph import START, StateGraph
+
+
+ class State(TypedDict):
+ x: int
+
+
+ def my_node(state: State, config: RunnableConfig) -> State:
+ return {"x": state["x"] + 1}
+
+
+ builder = StateGraph(State)
+ builder.add_node("my_fair_node", my_node)
+ builder.add_edge(START, "my_fair_node")
+ graph = builder.compile()
+ graph.invoke({"x": 1})
+ # {'x': 2}
+ ```
+
+ Returns:
+ Self: The instance of the `StateGraph`, allowing for method chaining.
+ """
+ ...
+
+ @overload
+ def add_node(
+ self,
+ node: str | StateNode[NodeInputT, ContextT],
+ action: StateNode[NodeInputT, ContextT] | None = None,
+ *,
+ defer: bool = False,
+ metadata: dict[str, Any] | None = None,
+ input_schema: type[NodeInputT],
+ retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
+ cache_policy: CachePolicy | None = None,
+ destinations: dict[str, str] | tuple[str, ...] | None = None,
+ **kwargs: Unpack[DeprecatedKwargs],
+ ) -> Self:
+ """Add a new node to the `StateGraph`, input schema is specified.
+
+ Args:
+ node: The function or runnable this node will run.
+
+ If a string is provided, it will be used as the node name, and action will be used as the function or runnable.
+ action: The action associated with the node.
+
+ Will be used as the node function or runnable if `node` is a string (node name).
+ defer: Whether to defer the execution of the node until the run is about to end.
+ metadata: The metadata associated with the node.
+ input_schema: The input schema for the node.
+ retry_policy: The retry policy for the node.
+
+ If a sequence is provided, the first matching policy will be applied.
+ cache_policy: The cache policy for the node.
+ destinations: Destinations that indicate where a node can route to.
+
+ Useful for edgeless graphs with nodes that return `Command` objects.
+
+ If a `dict` is provided, the keys will be used as the target node names and the values will be used as the labels for the edges.
+
+ If a `tuple` is provided, the values will be used as the target node names.
+
+ !!! warning
+
+ This is only used for graph rendering and doesn't have any effect on the graph execution.
+
+ Example:
+ ```python
+ from typing_extensions import TypedDict
+
+ from langchain_core.runnables import RunnableConfig
+ from langgraph.graph import START, StateGraph
+
+
+ class State(TypedDict):
+ x: int
+
+
+ class NodeInput(TypedDict):
+ x: int
+
+
+ def my_node(state: NodeInput, config: RunnableConfig) -> State:
+ return {"x": state["x"] + 1}
+
+
+ builder = StateGraph(State)
+ builder.add_node("my_fair_node", my_node, input_schema=NodeInput)
+ builder.add_edge(START, "my_fair_node")
+ graph = builder.compile()
+ graph.invoke({"x": 1})
+ # {'x': 2}
+ ```
+
+ Returns:
+ Self: The instance of the `StateGraph`, allowing for method chaining.
+ """
+ ...
+
+ def add_node(
+ self,
+ node: str | StateNode[NodeInputT, ContextT],
+ action: StateNode[NodeInputT, ContextT] | None = None,
+ *,
+ defer: bool = False,
+ metadata: dict[str, Any] | None = None,
+ input_schema: type[NodeInputT] | None = None,
+ retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
+ cache_policy: CachePolicy | None = None,
+ destinations: dict[str, str] | tuple[str, ...] | None = None,
+ **kwargs: Unpack[DeprecatedKwargs],
+ ) -> Self:
+ """Add a new node to the `StateGraph`.
+
+ Args:
+ node: The function or runnable this node will run.
+
+ If a string is provided, it will be used as the node name, and action will be used as the function or runnable.
+ action: The action associated with the node.
+
+ Will be used as the node function or runnable if `node` is a string (node name).
+ defer: Whether to defer the execution of the node until the run is about to end.
+ metadata: The metadata associated with the node.
+ input_schema: The input schema for the node. (Default: the graph's state schema)
+ retry_policy: The retry policy for the node.
+
+ If a sequence is provided, the first matching policy will be applied.
+ cache_policy: The cache policy for the node.
+ destinations: Destinations that indicate where a node can route to.
+
+ Useful for edgeless graphs with nodes that return `Command` objects.
+
+ If a `dict` is provided, the keys will be used as the target node names and the values will be used as the labels for the edges.
+
+ If a `tuple` is provided, the values will be used as the target node names.
+
+ !!! warning
+
+ This is only used for graph rendering and doesn't have any effect on the graph execution.
+
+ Example:
+ ```python
+ from typing_extensions import TypedDict
+
+ from langchain_core.runnables import RunnableConfig
+ from langgraph.graph import START, StateGraph
+
+
+ class State(TypedDict):
+ x: int
+
+
+ def my_node(state: State, config: RunnableConfig) -> State:
+ return {"x": state["x"] + 1}
+
+
+ builder = StateGraph(State)
+ builder.add_node(my_node) # node name will be 'my_node'
+ builder.add_edge(START, "my_node")
+ graph = builder.compile()
+ graph.invoke({"x": 1})
+ # {'x': 2}
+ ```
+
+ Example: Customize the name:
+ ```python
+ builder = StateGraph(State)
+ builder.add_node("my_fair_node", my_node)
+ builder.add_edge(START, "my_fair_node")
+ graph = builder.compile()
+ graph.invoke({"x": 1})
+ # {'x': 2}
+ ```
+
+ Returns:
+ Self: The instance of the `StateGraph`, allowing for method chaining.
+ """
+ if (retry := kwargs.get("retry", MISSING)) is not MISSING:
+ warnings.warn(
+ "`retry` is deprecated and will be removed. Please use `retry_policy` instead.",
+ category=LangGraphDeprecatedSinceV05,
+ )
+ if retry_policy is None:
+ retry_policy = retry # type: ignore[assignment]
+
+ if (input_ := kwargs.get("input", MISSING)) is not MISSING:
+ warnings.warn(
+ "`input` is deprecated and will be removed. Please use `input_schema` instead.",
+ category=LangGraphDeprecatedSinceV05,
+ )
+ if input_schema is None:
+ input_schema = cast(type[NodeInputT] | None, input_)
+
+ if not isinstance(node, str):
+ action = node
+ if isinstance(action, Runnable):
+ node = action.get_name()
+ else:
+ node = getattr(action, "__name__", action.__class__.__name__)
+ if node is None:
+ raise ValueError(
+ "Node name must be provided if action is not a function"
+ )
+ if self.compiled:
+ logger.warning(
+ "Adding a node to a graph that has already been compiled. This will "
+ "not be reflected in the compiled graph."
+ )
+ if not isinstance(node, str):
+ action = node
+ node = cast(str, getattr(action, "name", getattr(action, "__name__", None)))
+ if node is None:
+ raise ValueError(
+ "Node name must be provided if action is not a function"
+ )
+ if action is None:
+ raise RuntimeError
+ if node in self.nodes:
+ raise ValueError(f"Node `{node}` already present.")
+ if node == END or node == START:
+ raise ValueError(f"Node `{node}` is reserved.")
+
+ for character in (NS_SEP, NS_END):
+ if character in node:
+ raise ValueError(
+ f"'{character}' is a reserved character and is not allowed in the node names."
+ )
+
+ inferred_input_schema = None
+
+ ends: tuple[str, ...] | dict[str, str] = EMPTY_SEQ
+ try:
+ if (
+ isfunction(action)
+ or ismethod(action)
+ or ismethod(getattr(action, "__call__", None))
+ ) and (
+ hints := get_type_hints(getattr(action, "__call__"))
+ or get_type_hints(action)
+ ):
+ if input_schema is None:
+ first_parameter_name = next(
+ iter(
+ inspect.signature(
+ cast(FunctionType, action)
+ ).parameters.keys()
+ )
+ )
+ if input_hint := hints.get(first_parameter_name):
+ if isinstance(input_hint, type) and get_type_hints(input_hint):
+ inferred_input_schema = input_hint
+ if rtn := hints.get("return"):
+ # Handle Union types
+ rtn_origin = get_origin(rtn)
+ if rtn_origin is Union:
+ rtn_args = get_args(rtn)
+ # Look for Command in the union
+ for arg in rtn_args:
+ arg_origin = get_origin(arg)
+ if arg_origin is Command:
+ rtn = arg
+ rtn_origin = arg_origin
+ break
+
+ # Check if it's a Command type
+ if (
+ rtn_origin is Command
+ and (rargs := get_args(rtn))
+ and get_origin(rargs[0]) is Literal
+ and (vals := get_args(rargs[0]))
+ ):
+ ends = vals
+ except (NameError, TypeError, StopIteration):
+ pass
+
+ if destinations is not None:
+ ends = destinations
+
+ if input_schema is not None:
+ self.nodes[node] = StateNodeSpec[NodeInputT, ContextT](
+ coerce_to_runnable(action, name=node, trace=False), # type: ignore[arg-type]
+ metadata,
+ input_schema=input_schema,
+ retry_policy=retry_policy,
+ cache_policy=cache_policy,
+ ends=ends,
+ defer=defer,
+ )
+ elif inferred_input_schema is not None:
+ self.nodes[node] = StateNodeSpec(
+ coerce_to_runnable(action, name=node, trace=False), # type: ignore[arg-type]
+ metadata,
+ input_schema=inferred_input_schema,
+ retry_policy=retry_policy,
+ cache_policy=cache_policy,
+ ends=ends,
+ defer=defer,
+ )
+ else:
+ self.nodes[node] = StateNodeSpec[StateT, ContextT](
+ coerce_to_runnable(action, name=node, trace=False), # type: ignore[arg-type]
+ metadata,
+ input_schema=self.state_schema,
+ retry_policy=retry_policy,
+ cache_policy=cache_policy,
+ ends=ends,
+ defer=defer,
+ )
+
+ input_schema = input_schema or inferred_input_schema
+ if input_schema is not None:
+ self._add_schema(input_schema)
+
+ return self
+
+ def add_edge(self, start_key: str | list[str], end_key: str) -> Self:
+ """Add a directed edge from the start node (or list of start nodes) to the end node.
+
+ When a single start node is provided, the graph will wait for that node to complete
+ before executing the end node. When multiple start nodes are provided,
+ the graph will wait for ALL of the start nodes to complete before executing the end node.
+
+ Args:
+ start_key: The key(s) of the start node(s) of the edge.
+ end_key: The key of the end node of the edge.
+
+ Raises:
+ ValueError: If the start key is `'END'` or if the start key or end key is not present in the graph.
+
+ Returns:
+ Self: The instance of the `StateGraph`, allowing for method chaining.
+ """
+ if self.compiled:
+ logger.warning(
+ "Adding an edge to a graph that has already been compiled. This will "
+ "not be reflected in the compiled graph."
+ )
+
+ if isinstance(start_key, str):
+ if start_key == END:
+ raise ValueError("END cannot be a start node")
+ if end_key == START:
+ raise ValueError("START cannot be an end node")
+
+ # run this validation only for non-StateGraph graphs
+ if not hasattr(self, "channels") and start_key in set(
+ start for start, _ in self.edges
+ ):
+ raise ValueError(
+ f"Already found path for node '{start_key}'.\n"
+ "For multiple edges, use StateGraph with an Annotated state key."
+ )
+
+ self.edges.add((start_key, end_key))
+ return self
+
+ for start in start_key:
+ if start == END:
+ raise ValueError("END cannot be a start node")
+ if start not in self.nodes:
+ raise ValueError(f"Need to add_node `{start}` first")
+ if end_key == START:
+ raise ValueError("START cannot be an end node")
+ if end_key != END and end_key not in self.nodes:
+ raise ValueError(f"Need to add_node `{end_key}` first")
+
+ self.waiting_edges.add((tuple(start_key), end_key))
+ return self
+
+ def add_conditional_edges(
+ self,
+ source: str,
+ path: Callable[..., Hashable | Sequence[Hashable]]
+ | Callable[..., Awaitable[Hashable | Sequence[Hashable]]]
+ | Runnable[Any, Hashable | Sequence[Hashable]],
+ path_map: dict[Hashable, str] | list[str] | None = None,
+ ) -> Self:
+ """Add a conditional edge from the starting node to any number of destination nodes.
+
+ Args:
+ source: The starting node. This conditional edge will run when
+ exiting this node.
+ path: The callable that determines the next node or nodes.
+
+ If not specifying `path_map` it should return one or more nodes.
+
+ If it returns `'END'`, the graph will stop execution.
+ path_map: Optional mapping of paths to node names.
+
+ If omitted the paths returned by `path` should be node names.
+
+ Returns:
+ Self: The instance of the graph, allowing for method chaining.
+
+ !!! warning
+ Without type hints on the `path` function's return value (e.g., `-> Literal["foo", "__end__"]:`)
+ or a path_map, the graph visualization assumes the edge could transition to any node in the graph.
+
+ """ # noqa: E501
+ if self.compiled:
+ logger.warning(
+ "Adding an edge to a graph that has already been compiled. This will "
+ "not be reflected in the compiled graph."
+ )
+
+ # find a name for the condition
+ path = coerce_to_runnable(path, name=None, trace=True)
+ name = path.name or "condition"
+ # validate the condition
+ if name in self.branches[source]:
+ raise ValueError(
+ f"Branch with name `{path.name}` already exists for node `{source}`"
+ )
+ # save it
+ self.branches[source][name] = BranchSpec.from_path(path, path_map, True)
+ if schema := self.branches[source][name].input_schema:
+ self._add_schema(schema)
+ return self
+
+ def add_sequence(
+ self,
+ nodes: Sequence[
+ StateNode[NodeInputT, ContextT]
+ | tuple[str, StateNode[NodeInputT, ContextT]]
+ ],
+ ) -> Self:
+ """Add a sequence of nodes that will be executed in the provided order.
+
+ Args:
+ nodes: A sequence of `StateNode` (callables that accept a `state` arg) or `(name, StateNode)` tuples.
+
+ If no names are provided, the name will be inferred from the node object (e.g. a `Runnable` or a `Callable` name).
+
+ Each node will be executed in the order provided.
+
+ Raises:
+ ValueError: If the sequence is empty.
+ ValueError: If the sequence contains duplicate node names.
+
+ Returns:
+ Self: The instance of the `StateGraph`, allowing for method chaining.
+ """
+ if len(nodes) < 1:
+ raise ValueError("Sequence requires at least one node.")
+
+ previous_name: str | None = None
+ for node in nodes:
+ if isinstance(node, tuple) and len(node) == 2:
+ name, node = node
+ else:
+ name = _get_node_name(node)
+
+ if name in self.nodes:
+ raise ValueError(
+ f"Node names must be unique: node with the name '{name}' already exists. "
+ "If you need to use two different runnables/callables with the same name (for example, using `lambda`), please provide them as tuples (name, runnable/callable)."
+ )
+
+ self.add_node(name, node)
+ if previous_name is not None:
+ self.add_edge(previous_name, name)
+
+ previous_name = name
+
+ return self
+
+ def set_entry_point(self, key: str) -> Self:
+ """Specifies the first node to be called in the graph.
+
+ Equivalent to calling `add_edge(START, key)`.
+
+ Parameters:
+ key (str): The key of the node to set as the entry point.
+
+ Returns:
+ Self: The instance of the graph, allowing for method chaining.
+ """
+ return self.add_edge(START, key)
+
+ def set_conditional_entry_point(
+ self,
+ path: Callable[..., Hashable | Sequence[Hashable]]
+ | Callable[..., Awaitable[Hashable | Sequence[Hashable]]]
+ | Runnable[Any, Hashable | Sequence[Hashable]],
+ path_map: dict[Hashable, str] | list[str] | None = None,
+ ) -> Self:
+ """Sets a conditional entry point in the graph.
+
+ Args:
+ path: The callable that determines the next node or nodes.
+
+ If not specifying `path_map` it should return one or more nodes.
+
+ If it returns END, the graph will stop execution.
+ path_map: Optional mapping of paths to node names.
+
+ If omitted the paths returned by `path` should be node names.
+
+ Returns:
+ Self: The instance of the graph, allowing for method chaining.
+ """
+ return self.add_conditional_edges(START, path, path_map)
+
+ def set_finish_point(self, key: str) -> Self:
+ """Marks a node as a finish point of the graph.
+
+ If the graph reaches this node, it will cease execution.
+
+ Parameters:
+ key (str): The key of the node to set as the finish point.
+
+ Returns:
+ Self: The instance of the graph, allowing for method chaining.
+ """
+ return self.add_edge(key, END)
+
+ def validate(self, interrupt: Sequence[str] | None = None) -> Self:
+ # assemble sources
+ all_sources = {src for src, _ in self._all_edges}
+ for start, branches in self.branches.items():
+ all_sources.add(start)
+ for name, spec in self.nodes.items():
+ if spec.ends:
+ all_sources.add(name)
+ # validate sources
+ for source in all_sources:
+ 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():
+ for cond, branch in branches.items():
+ if branch.ends is not None:
+ for end in branch.ends.values():
+ if end not in self.nodes and end != END:
+ raise ValueError(
+ f"At '{start}' node, '{cond}' branch found unknown target '{end}'"
+ )
+ all_targets.add(end)
+ else:
+ all_targets.add(END)
+ for node in self.nodes:
+ if node != start:
+ all_targets.add(node)
+ for name, spec in self.nodes.items():
+ if spec.ends:
+ all_targets.update(spec.ends)
+ for target in all_targets:
+ if target not in self.nodes and target != END:
+ raise ValueError(f"Found edge ending at unknown node `{target}`")
+ # validate interrupts
+ if interrupt:
+ for node in interrupt:
+ if node not in self.nodes:
+ raise ValueError(f"Interrupt node `{node}` not found")
+
+ self.compiled = True
+ return self
+
+ def compile(
+ self,
+ checkpointer: Checkpointer = None,
+ *,
+ cache: BaseCache | None = None,
+ store: BaseStore | None = None,
+ interrupt_before: All | list[str] | None = None,
+ interrupt_after: All | list[str] | None = None,
+ debug: bool = False,
+ name: str | None = None,
+ ) -> CompiledStateGraph[StateT, ContextT, InputT, OutputT]:
+ """Compiles the `StateGraph` into a `CompiledStateGraph` object.
+
+ The compiled graph implements the `Runnable` interface and can be invoked,
+ streamed, batched, and run asynchronously.
+
+ Args:
+ checkpointer: A checkpoint saver object or flag.
+
+ If provided, this `Checkpointer` serves as a fully versioned "short-term memory" for the graph,
+ allowing it to be paused, resumed, and replayed from any point.
+
+ If `None`, it may inherit the parent graph's checkpointer when used as a subgraph.
+
+ If `False`, it will not use or inherit any checkpointer.
+
+ **Important**: When a checkpointer is enabled, you should pass a `thread_id`
+ in the config when invoking the graph:
+
+ ```python
+ config = {"configurable": {"thread_id": "my-thread"}}
+ graph.invoke(inputs, config)
+ ```
+
+ The `thread_id` is the key used to store and retrieve checkpoints. Use a
+ unique ID for independent runs, or reuse the same ID to accumulate state
+ across invocations (e.g., for conversation memory).
+
+ interrupt_before: An optional list of node names to interrupt before.
+ interrupt_after: An optional list of node names to interrupt after.
+ debug: A flag indicating whether to enable debug mode.
+ name: The name to use for the compiled graph.
+
+ Returns:
+ CompiledStateGraph: The compiled `StateGraph`.
+ """
+ checkpointer = ensure_valid_checkpointer(checkpointer)
+
+ # assign default values
+ interrupt_before = interrupt_before or []
+ interrupt_after = interrupt_after or []
+
+ # validate the graph
+ self.validate(
+ interrupt=(
+ (interrupt_before if interrupt_before != "*" else []) + interrupt_after
+ if interrupt_after != "*"
+ else []
+ )
+ )
+
+ # prepare output channels
+ output_channels = (
+ "__root__"
+ if len(self.schemas[self.output_schema]) == 1
+ and "__root__" in self.schemas[self.output_schema]
+ else [
+ key
+ for key, val in self.schemas[self.output_schema].items()
+ if not is_managed_value(val)
+ ]
+ )
+ stream_channels = (
+ "__root__"
+ if len(self.channels) == 1 and "__root__" in self.channels
+ else [
+ key for key, val in self.channels.items() if not is_managed_value(val)
+ ]
+ )
+
+ compiled = CompiledStateGraph[StateT, ContextT, InputT, OutputT](
+ builder=self,
+ schema_to_mapper={},
+ context_schema=self.context_schema,
+ nodes={},
+ channels={
+ **self.channels,
+ **self.managed,
+ START: EphemeralValue(self.input_schema),
+ },
+ input_channels=START,
+ stream_mode="updates",
+ output_channels=output_channels,
+ stream_channels=stream_channels,
+ checkpointer=checkpointer,
+ interrupt_before_nodes=interrupt_before,
+ interrupt_after_nodes=interrupt_after,
+ auto_validate=False,
+ debug=debug,
+ store=store,
+ cache=cache,
+ name=name or "LangGraph",
+ )
+
+ compiled.attach_node(START, None)
+ for key, node in self.nodes.items():
+ compiled.attach_node(key, node)
+
+ for start, end in self.edges:
+ compiled.attach_edge(start, end)
+
+ for starts, end in self.waiting_edges:
+ compiled.attach_edge(starts, end)
+
+ for start, branches in self.branches.items():
+ for name, branch in branches.items():
+ compiled.attach_branch(start, name, branch)
+
+ return compiled.validate()
+
+
+class CompiledStateGraph(
+ Pregel[StateT, ContextT, InputT, OutputT],
+ Generic[StateT, ContextT, InputT, OutputT],
+):
+ builder: StateGraph[StateT, ContextT, InputT, OutputT]
+ schema_to_mapper: dict[type[Any], Callable[[Any], Any] | None]
+
+ def __init__(
+ self,
+ *,
+ builder: StateGraph[StateT, ContextT, InputT, OutputT],
+ schema_to_mapper: dict[type[Any], Callable[[Any], Any] | None],
+ **kwargs: Any,
+ ) -> None:
+ super().__init__(**kwargs)
+ self.builder = builder
+ self.schema_to_mapper = schema_to_mapper
+
+ def get_input_jsonschema(
+ self, config: RunnableConfig | None = None
+ ) -> dict[str, Any]:
+ return _get_json_schema(
+ typ=self.builder.input_schema,
+ schemas=self.builder.schemas,
+ channels=self.builder.channels,
+ name=self.get_name("Input"),
+ )
+
+ def get_output_jsonschema(
+ self, config: RunnableConfig | None = None
+ ) -> dict[str, Any]:
+ return _get_json_schema(
+ typ=self.builder.output_schema,
+ schemas=self.builder.schemas,
+ channels=self.builder.channels,
+ name=self.get_name("Output"),
+ )
+
+ def attach_node(self, key: str, node: StateNodeSpec[Any, ContextT] | None) -> None:
+ if key == START:
+ output_keys = [
+ k
+ for k, v in self.builder.schemas[self.builder.input_schema].items()
+ if not is_managed_value(v)
+ ]
+ else:
+ output_keys = list(self.builder.channels) + [
+ k for k, v in self.builder.managed.items()
+ ]
+
+ def _get_updates(
+ input: None | dict | Any,
+ ) -> Sequence[tuple[str, Any]] | None:
+ if input is None:
+ return None
+ elif isinstance(input, dict):
+ return [(k, v) for k, v in input.items() if k in output_keys]
+ elif isinstance(input, Command):
+ if input.graph == Command.PARENT:
+ return None
+ return [
+ (k, v) for k, v in input._update_as_tuples() if k in output_keys
+ ]
+ elif (
+ isinstance(input, (list, tuple))
+ and input
+ and any(isinstance(i, Command) for i in input)
+ ):
+ updates: list[tuple[str, Any]] = []
+ for i in input:
+ if isinstance(i, Command):
+ if i.graph == Command.PARENT:
+ continue
+ updates.extend(
+ (k, v) for k, v in i._update_as_tuples() if k in output_keys
+ )
+ else:
+ updates.extend(_get_updates(i) or ())
+ return updates
+ elif (t := type(input)) and get_cached_annotated_keys(t):
+ return get_update_as_tuples(input, output_keys)
+ else:
+ msg = create_error_message(
+ message=f"Expected dict, got {input}",
+ error_code=ErrorCode.INVALID_GRAPH_NODE_RETURN_VALUE,
+ )
+ raise InvalidUpdateError(msg)
+
+ # state updaters
+ write_entries: tuple[ChannelWriteEntry | ChannelWriteTupleEntry, ...] = (
+ ChannelWriteTupleEntry(
+ mapper=_get_root if output_keys == ["__root__"] else _get_updates
+ ),
+ ChannelWriteTupleEntry(
+ mapper=_control_branch,
+ static=_control_static(node.ends)
+ if node is not None and node.ends is not None
+ else None,
+ ),
+ )
+
+ # add node and output channel
+ if key == START:
+ self.nodes[key] = PregelNode(
+ tags=[TAG_HIDDEN],
+ triggers=[START],
+ channels=START,
+ writers=[ChannelWrite(write_entries)],
+ )
+ elif node is not None:
+ input_schema = node.input_schema if node else self.builder.state_schema
+ input_channels = list(self.builder.schemas[input_schema])
+ is_single_input = len(input_channels) == 1 and "__root__" in input_channels
+ if input_schema in self.schema_to_mapper:
+ mapper = self.schema_to_mapper[input_schema]
+ else:
+ mapper = _pick_mapper(input_channels, input_schema)
+ self.schema_to_mapper[input_schema] = mapper
+
+ branch_channel = _CHANNEL_BRANCH_TO.format(key)
+ self.channels[branch_channel] = (
+ LastValueAfterFinish(Any)
+ if node.defer
+ else EphemeralValue(Any, guard=False)
+ )
+ self.nodes[key] = PregelNode(
+ triggers=[branch_channel],
+ # read state keys and managed values
+ channels=("__root__" if is_single_input else input_channels),
+ # coerce state dict to schema class (eg. pydantic model)
+ mapper=mapper,
+ # publish to state keys
+ writers=[ChannelWrite(write_entries)],
+ metadata=node.metadata,
+ retry_policy=node.retry_policy,
+ cache_policy=node.cache_policy,
+ bound=node.runnable, # type: ignore[arg-type]
+ )
+ else:
+ raise RuntimeError
+
+ def attach_edge(self, starts: str | Sequence[str], end: str) -> None:
+ if isinstance(starts, str):
+ # subscribe to start channel
+ if end != END:
+ self.nodes[starts].writers.append(
+ ChannelWrite(
+ (ChannelWriteEntry(_CHANNEL_BRANCH_TO.format(end), None),)
+ )
+ )
+ elif end != END:
+ channel_name = f"join:{'+'.join(starts)}:{end}"
+ # register channel
+ if self.builder.nodes[end].defer:
+ self.channels[channel_name] = NamedBarrierValueAfterFinish(
+ str, set(starts)
+ )
+ else:
+ self.channels[channel_name] = NamedBarrierValue(str, set(starts))
+ # subscribe to channel
+ self.nodes[end].triggers.append(channel_name)
+ # publish to channel
+ for start in starts:
+ self.nodes[start].writers.append(
+ ChannelWrite((ChannelWriteEntry(channel_name, start),))
+ )
+
+ def attach_branch(
+ self, start: str, name: str, branch: BranchSpec, *, with_reader: bool = True
+ ) -> None:
+ def get_writes(
+ packets: Sequence[str | Send], static: bool = False
+ ) -> Sequence[ChannelWriteEntry | Send]:
+ writes = [
+ (
+ ChannelWriteEntry(
+ p if p == END else _CHANNEL_BRANCH_TO.format(p), None
+ )
+ if not isinstance(p, Send)
+ else p
+ )
+ for p in packets
+ if (True if static else p != END)
+ ]
+ if not writes:
+ return []
+ return writes
+
+ if with_reader:
+ # get schema
+ schema = branch.input_schema or (
+ self.builder.nodes[start].input_schema
+ if start in self.builder.nodes
+ else self.builder.state_schema
+ )
+ channels = list(self.builder.schemas[schema])
+ # get mapper
+ if schema in self.schema_to_mapper:
+ mapper = self.schema_to_mapper[schema]
+ else:
+ mapper = _pick_mapper(channels, schema)
+ self.schema_to_mapper[schema] = mapper
+ # create reader
+ reader: Callable[[RunnableConfig], Any] | None = partial(
+ ChannelRead.do_read,
+ select=channels[0] if channels == ["__root__"] else channels,
+ fresh=True,
+ # coerce state dict to schema class (eg. pydantic model)
+ mapper=mapper,
+ )
+ else:
+ reader = None
+
+ # attach branch publisher
+ self.nodes[start].writers.append(branch.run(get_writes, reader))
+
+ def _migrate_checkpoint(self, checkpoint: Checkpoint) -> None:
+ """Migrate a checkpoint to new channel layout."""
+ super()._migrate_checkpoint(checkpoint)
+
+ values = checkpoint["channel_values"]
+ versions = checkpoint["channel_versions"]
+ seen = checkpoint["versions_seen"]
+
+ # empty checkpoints do not need migration
+ if not versions:
+ return
+
+ # current version
+ if checkpoint["v"] >= 3:
+ return
+
+ # Migrate from start:node to branch:to:node
+ for k in list(versions):
+ if k.startswith("start:"):
+ # confirm node is present
+ node = k.split(":")[1]
+ if node not in self.nodes:
+ continue
+ # get next version
+ new_k = f"branch:to:{node}"
+ new_v = (
+ max(versions[new_k], versions.pop(k))
+ if new_k in versions
+ else versions.pop(k)
+ )
+ # update seen
+ for ss in (seen.get(node, {}), seen.get(INTERRUPT, {})):
+ if k in ss:
+ s = ss.pop(k)
+ if new_k in ss:
+ ss[new_k] = max(s, ss[new_k])
+ else:
+ ss[new_k] = s
+ # update value
+ if new_k not in values and k in values:
+ values[new_k] = values.pop(k)
+ # update version
+ versions[new_k] = new_v
+
+ # Migrate from branch:source:condition:node to branch:to:node
+ for k in list(versions):
+ if k.startswith("branch:") and k.count(":") == 3:
+ # confirm node is present
+ node = k.split(":")[-1]
+ if node not in self.nodes:
+ continue
+ # get next version
+ new_k = f"branch:to:{node}"
+ new_v = (
+ max(versions[new_k], versions.pop(k))
+ if new_k in versions
+ else versions.pop(k)
+ )
+ # update seen
+ for ss in (seen.get(node, {}), seen.get(INTERRUPT, {})):
+ if k in ss:
+ s = ss.pop(k)
+ if new_k in ss:
+ ss[new_k] = max(s, ss[new_k])
+ else:
+ ss[new_k] = s
+ # update value
+ if new_k not in values and k in values:
+ values[new_k] = values.pop(k)
+ # update version
+ versions[new_k] = new_v
+
+ if not set(self.nodes).isdisjoint(versions):
+ # Migrate from "node" to "branch:to:node"
+ source_to_target = defaultdict(list)
+ for start, end in self.builder.edges:
+ if start != START and end != END:
+ source_to_target[start].append(end)
+ for k in list(versions):
+ if k == START:
+ continue
+ if k in self.nodes:
+ v = versions.pop(k)
+ c = values.pop(k, MISSING)
+ for end in source_to_target[k]:
+ # get next version
+ new_k = f"branch:to:{end}"
+ new_v = max(versions[new_k], v) if new_k in versions else v
+ # update seen
+ for ss in (seen.get(end, {}), seen.get(INTERRUPT, {})):
+ if k in ss:
+ s = ss.pop(k)
+ if new_k in ss:
+ ss[new_k] = max(s, ss[new_k])
+ else:
+ ss[new_k] = s
+ # update value
+ if new_k not in values and c is not MISSING:
+ values[new_k] = c
+ # update version
+ versions[new_k] = new_v
+ # pop interrupt seen
+ if INTERRUPT in seen:
+ seen[INTERRUPT].pop(k, MISSING)
+
+
+def _pick_mapper(
+ state_keys: Sequence[str], schema: type[Any]
+) -> Callable[[Any], Any] | None:
+ if state_keys == ["__root__"]:
+ return None
+ if isclass(schema) and issubclass(schema, dict):
+ return None
+ return partial(_coerce_state, schema)
+
+
+def _coerce_state(schema: type[Any], input: dict[str, Any]) -> dict[str, Any]:
+ return schema(**input)
+
+
+def _control_branch(value: Any) -> Sequence[tuple[str, Any]]:
+ if isinstance(value, Send):
+ return ((TASKS, value),)
+ commands: list[Command] = []
+ if isinstance(value, Command):
+ commands.append(value)
+ elif isinstance(value, (list, tuple)):
+ for cmd in value:
+ if isinstance(cmd, Command):
+ commands.append(cmd)
+ rtn: list[tuple[str, Any]] = []
+ for command in commands:
+ if command.graph == Command.PARENT:
+ raise ParentCommand(command)
+
+ goto_targets = (
+ [command.goto] if isinstance(command.goto, (Send, str)) else command.goto
+ )
+
+ for go in goto_targets:
+ if isinstance(go, Send):
+ rtn.append((TASKS, go))
+ elif isinstance(go, str) and go != END:
+ # END is a special case, it's not actually a node in a practical sense
+ # but rather a special terminal node that we don't need to branch to
+ rtn.append((_CHANNEL_BRANCH_TO.format(go), None))
+ return rtn
+
+
+def _control_static(
+ ends: tuple[str, ...] | dict[str, str],
+) -> Sequence[tuple[str, Any, str | None]]:
+ if isinstance(ends, dict):
+ return [
+ (k if k == END else _CHANNEL_BRANCH_TO.format(k), None, label)
+ for k, label in ends.items()
+ ]
+ else:
+ return [
+ (e if e == END else _CHANNEL_BRANCH_TO.format(e), None, None) for e in ends
+ ]
+
+
+def _get_root(input: Any) -> Sequence[tuple[str, Any]] | None:
+ if isinstance(input, Command):
+ if input.graph == Command.PARENT:
+ return ()
+ return input._update_as_tuples()
+ elif (
+ isinstance(input, (list, tuple))
+ and input
+ and any(isinstance(i, Command) for i in input)
+ ):
+ updates: list[tuple[str, Any]] = []
+ for i in input:
+ if isinstance(i, Command):
+ if i.graph == Command.PARENT:
+ continue
+ updates.extend(i._update_as_tuples())
+ else:
+ updates.append(("__root__", i))
+ return updates
+ elif input is not None:
+ return [("__root__", input)]
+
+
+def _get_channels(
+ schema: type[dict],
+) -> tuple[dict[str, BaseChannel], dict[str, ManagedValueSpec], dict[str, Any]]:
+ if not hasattr(schema, "__annotations__"):
+ return (
+ {"__root__": _get_channel("__root__", schema, allow_managed=False)},
+ {},
+ {},
+ )
+
+ type_hints = get_type_hints(schema, include_extras=True)
+ all_keys = {
+ name: _get_channel(name, typ)
+ for name, typ in type_hints.items()
+ if name != "__slots__"
+ }
+ return (
+ {k: v for k, v in all_keys.items() if isinstance(v, BaseChannel)},
+ {k: v for k, v in all_keys.items() if is_managed_value(v)},
+ type_hints,
+ )
+
+
+@overload
+def _get_channel(
+ name: str, annotation: Any, *, allow_managed: Literal[False]
+) -> BaseChannel: ...
+
+
+@overload
+def _get_channel(
+ name: str, annotation: Any, *, allow_managed: Literal[True] = True
+) -> BaseChannel | ManagedValueSpec: ...
+
+
+def _get_channel(
+ name: str, annotation: Any, *, allow_managed: bool = True
+) -> BaseChannel | ManagedValueSpec:
+ # Strip out Required and NotRequired wrappers
+ if hasattr(annotation, "__origin__") and annotation.__origin__ in (
+ Required,
+ NotRequired,
+ ):
+ annotation = annotation.__args__[0]
+ if manager := _is_field_managed_value(name, annotation):
+ if allow_managed:
+ return manager
+ else:
+ raise ValueError(f"This {annotation} not allowed in this position")
+ elif channel := _is_field_channel(annotation):
+ channel.key = name
+ return channel
+ elif channel := _is_field_binop(annotation):
+ channel.key = name
+ return channel
+
+ fallback: LastValue = LastValue(annotation)
+ fallback.key = name
+ return fallback
+
+
+def _is_field_channel(typ: type[Any]) -> BaseChannel | None:
+ if hasattr(typ, "__metadata__"):
+ meta = typ.__metadata__
+ # Search through all annotated medata to find channel annotations
+ for item in meta:
+ if isinstance(item, BaseChannel):
+ return item
+ elif isclass(item) and issubclass(item, BaseChannel):
+ # ex, Annotated[int, EphemeralValue, SomeOtherAnnotation]
+ # would return EphemeralValue(int)
+ return item(typ.__origin__ if hasattr(typ, "__origin__") else typ)
+ return None
+
+
+def _is_field_binop(typ: type[Any]) -> BinaryOperatorAggregate | None:
+ if hasattr(typ, "__metadata__"):
+ meta = typ.__metadata__
+ if len(meta) >= 1 and callable(meta[-1]):
+ sig = signature(meta[-1])
+ params = list(sig.parameters.values())
+ if (
+ sum(
+ p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
+ for p in params
+ )
+ == 2
+ ):
+ return BinaryOperatorAggregate(typ, meta[-1])
+ else:
+ raise ValueError(
+ f"Invalid reducer signature. Expected (a, b) -> c. Got {sig}"
+ )
+ return None
+
+
+def _is_field_managed_value(name: str, typ: type[Any]) -> ManagedValueSpec | None:
+ if hasattr(typ, "__metadata__"):
+ meta = typ.__metadata__
+ if len(meta) >= 1:
+ decoration = get_origin(meta[-1]) or meta[-1]
+ if is_managed_value(decoration):
+ return decoration
+
+ # Handle Required, NotRequired, etc wrapped types by extracting the inner type
+ if (
+ get_origin(typ) is not None
+ and (args := get_args(typ))
+ and (inner_type := args[0])
+ ):
+ return _is_field_managed_value(name, inner_type)
+
+ return None
+
+
+def _get_json_schema(
+ typ: type,
+ schemas: dict,
+ channels: dict,
+ name: str,
+) -> dict[str, Any]:
+ if isclass(typ) and issubclass(typ, BaseModel):
+ return typ.model_json_schema()
+ elif is_typeddict(typ):
+ return TypeAdapter(typ).json_schema()
+ else:
+ keys = list(schemas[typ].keys())
+ if len(keys) == 1 and keys[0] == "__root__":
+ return create_model(
+ name,
+ root=(channels[keys[0]].UpdateType, None),
+ ).model_json_schema()
+ else:
+ return create_model(
+ name,
+ field_definitions={
+ k: (
+ channels[k].UpdateType,
+ (
+ get_field_default(
+ k,
+ channels[k].UpdateType,
+ typ,
+ )
+ ),
+ )
+ for k in schemas[typ]
+ if k in channels and isinstance(channels[k], BaseChannel)
+ },
+ ).model_json_schema()
diff --git a/langgraph/source/libs/langgraph/langgraph/graph/ui.py b/langgraph/source/libs/langgraph/langgraph/graph/ui.py
new file mode 100644
index 0000000000000000000000000000000000000000..847b2c7fac4087e0dc5d25d70bf5d9272b718928
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/graph/ui.py
@@ -0,0 +1,227 @@
+from __future__ import annotations
+
+from typing import Any, Literal, cast
+from uuid import uuid4
+
+from langchain_core.messages import AnyMessage
+from typing_extensions import TypedDict
+
+from langgraph.config import get_config, get_stream_writer
+from langgraph.constants import CONF
+
+__all__ = (
+ "UIMessage",
+ "RemoveUIMessage",
+ "AnyUIMessage",
+ "push_ui_message",
+ "delete_ui_message",
+ "ui_message_reducer",
+)
+
+
+class UIMessage(TypedDict):
+ """A message type for UI updates in LangGraph.
+
+ This TypedDict represents a UI message that can be sent to update the UI state.
+ It contains information about the UI component to render and its properties.
+
+ Attributes:
+ type: Literal type indicating this is a UI message.
+ id: Unique identifier for the UI message.
+ name: Name of the UI component to render.
+ props: Properties to pass to the UI component.
+ metadata: Additional metadata about the UI message.
+ """
+
+ type: Literal["ui"]
+ id: str
+ name: str
+ props: dict[str, Any]
+ metadata: dict[str, Any]
+
+
+class RemoveUIMessage(TypedDict):
+ """A message type for removing UI components in LangGraph.
+
+ This TypedDict represents a message that can be sent to remove a UI component
+ from the current state.
+
+ Attributes:
+ type: Literal type indicating this is a remove-ui message.
+ id: Unique identifier of the UI message to remove.
+ """
+
+ type: Literal["remove-ui"]
+ id: str
+
+
+AnyUIMessage = UIMessage | RemoveUIMessage
+
+
+def push_ui_message(
+ name: str,
+ props: dict[str, Any],
+ *,
+ id: str | None = None,
+ metadata: dict[str, Any] | None = None,
+ message: AnyMessage | None = None,
+ state_key: str | None = "ui",
+ merge: bool = False,
+) -> UIMessage:
+ """Push a new UI message to update the UI state.
+
+ This function creates and sends a UI message that will be rendered in the UI.
+ It also updates the graph state with the new UI message.
+
+ Args:
+ name: Name of the UI component to render.
+ props: Properties to pass to the UI component.
+ id: Optional unique identifier for the UI message.
+ If not provided, a random UUID will be generated.
+ metadata: Optional additional metadata about the UI message.
+ message: Optional message object to associate with the UI message.
+ state_key: Key in the graph state where the UI messages are stored.
+ merge: Whether to merge props with existing UI message (True) or replace
+ them (False).
+
+ Returns:
+ The created UI message.
+
+ Example:
+ ```python
+ push_ui_message(
+ name="component-name",
+ props={"content": "Hello world"},
+ )
+ ```
+
+ """
+ from langgraph._internal._constants import CONFIG_KEY_SEND
+
+ writer = get_stream_writer()
+ config = get_config()
+
+ message_id = None
+ if message:
+ if isinstance(message, dict) and "id" in message:
+ message_id = message.get("id")
+ elif hasattr(message, "id"):
+ message_id = message.id
+
+ evt: UIMessage = {
+ "type": "ui",
+ "id": id or str(uuid4()),
+ "name": name,
+ "props": props,
+ "metadata": {
+ "merge": merge,
+ "run_id": config.get("run_id", None),
+ "tags": config.get("tags", None),
+ "name": config.get("run_name", None),
+ **(metadata or {}),
+ **({"message_id": message_id} if message_id else {}),
+ },
+ }
+
+ writer(evt)
+ if state_key:
+ config[CONF][CONFIG_KEY_SEND]([(state_key, evt)])
+
+ return evt
+
+
+def delete_ui_message(id: str, *, state_key: str = "ui") -> RemoveUIMessage:
+ """Delete a UI message by ID from the UI state.
+
+ This function creates and sends a message to remove a UI component from the current state.
+ It also updates the graph state to remove the UI message.
+
+ Args:
+ id: Unique identifier of the UI component to remove.
+ state_key: Key in the graph state where the UI messages are stored. Defaults to "ui".
+
+ Returns:
+ The remove UI message.
+
+ Example:
+ ```python
+ delete_ui_message("message-123")
+ ```
+
+ """
+ from langgraph._internal._constants import CONFIG_KEY_SEND
+
+ writer = get_stream_writer()
+ config = get_config()
+
+ evt: RemoveUIMessage = {"type": "remove-ui", "id": id}
+
+ writer(evt)
+ config[CONF][CONFIG_KEY_SEND]([(state_key, evt)])
+
+ return evt
+
+
+def ui_message_reducer(
+ left: list[AnyUIMessage] | AnyUIMessage,
+ right: list[AnyUIMessage] | AnyUIMessage,
+) -> list[AnyUIMessage]:
+ """Merge two lists of UI messages, supporting removing UI messages.
+
+ This function combines two lists of UI messages, handling both regular UI messages
+ and `remove-ui` messages. When a `remove-ui` message is encountered, it removes any
+ UI message with the matching ID from the current state.
+
+ Args:
+ left: First list of UI messages or single UI message.
+ right: Second list of UI messages or single UI message.
+
+ Returns:
+ Combined list of UI messages with removals applied.
+
+ Example:
+ ```python
+ messages = ui_message_reducer(
+ [{"type": "ui", "id": "1", "name": "Chat", "props": {}}],
+ {"type": "remove-ui", "id": "1"},
+ )
+ ```
+
+ """
+ if not isinstance(left, list):
+ left = [left]
+
+ if not isinstance(right, list):
+ right = [right]
+
+ # merge messages
+ merged = left.copy()
+ merged_by_id = {m.get("id"): i for i, m in enumerate(merged)}
+ ids_to_remove = set()
+
+ for msg in right:
+ msg_id = msg.get("id")
+
+ if (existing_idx := merged_by_id.get(msg_id)) is not None:
+ if msg.get("type") == "remove-ui":
+ ids_to_remove.add(msg_id)
+ else:
+ ids_to_remove.discard(msg_id)
+
+ if cast(UIMessage, msg).get("metadata", {}).get("merge", False):
+ prev_msg = merged[existing_idx]
+ msg = msg.copy()
+ msg["props"] = {**prev_msg["props"], **msg["props"]}
+
+ merged[existing_idx] = msg
+ else:
+ if msg.get("type") == "remove-ui":
+ raise ValueError(
+ f"Attempting to delete an UI message with an ID that doesn't exist ('{msg_id}')"
+ )
+
+ merged_by_id[msg_id] = len(merged)
+ merged.append(msg)
+
+ merged = [m for m in merged if m.get("id") not in ids_to_remove]
+ return merged
diff --git a/langgraph/source/libs/langgraph/langgraph/managed/__init__.py b/langgraph/source/libs/langgraph/langgraph/managed/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..2d50f323b2703f9747a7450d07f6eac868c665fd
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/managed/__init__.py
@@ -0,0 +1,3 @@
+from langgraph.managed.is_last_step import IsLastStep, RemainingSteps
+
+__all__ = ("IsLastStep", "RemainingSteps")
diff --git a/langgraph/source/libs/langgraph/langgraph/managed/base.py b/langgraph/source/libs/langgraph/langgraph/managed/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..7205967f29744a429a187a1b63dcb7f04a762ba2
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/managed/base.py
@@ -0,0 +1,31 @@
+from abc import ABC, abstractmethod
+from inspect import isclass
+from typing import (
+ Any,
+ Generic,
+ TypeGuard,
+ TypeVar,
+)
+
+from langgraph._internal._scratchpad import PregelScratchpad
+
+V = TypeVar("V")
+U = TypeVar("U")
+
+__all__ = ("ManagedValueSpec", "ManagedValueMapping")
+
+
+class ManagedValue(ABC, Generic[V]):
+ @staticmethod
+ @abstractmethod
+ def get(scratchpad: PregelScratchpad) -> V: ...
+
+
+ManagedValueSpec = type[ManagedValue]
+
+
+def is_managed_value(value: Any) -> TypeGuard[ManagedValueSpec]:
+ return isclass(value) and issubclass(value, ManagedValue)
+
+
+ManagedValueMapping = dict[str, ManagedValueSpec]
diff --git a/langgraph/source/libs/langgraph/langgraph/managed/is_last_step.py b/langgraph/source/libs/langgraph/langgraph/managed/is_last_step.py
new file mode 100644
index 0000000000000000000000000000000000000000..e53058db3660bca50e4c93ba4438ea02e25e1a07
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/managed/is_last_step.py
@@ -0,0 +1,24 @@
+from typing import Annotated
+
+from langgraph._internal._scratchpad import PregelScratchpad
+from langgraph.managed.base import ManagedValue
+
+__all__ = ("IsLastStep", "RemainingStepsManager")
+
+
+class IsLastStepManager(ManagedValue[bool]):
+ @staticmethod
+ def get(scratchpad: PregelScratchpad) -> bool:
+ return scratchpad.step == scratchpad.stop - 1
+
+
+IsLastStep = Annotated[bool, IsLastStepManager]
+
+
+class RemainingStepsManager(ManagedValue[int]):
+ @staticmethod
+ def get(scratchpad: PregelScratchpad) -> int:
+ return scratchpad.stop - scratchpad.step
+
+
+RemainingSteps = Annotated[int, RemainingStepsManager]
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/__init__.py b/langgraph/source/libs/langgraph/langgraph/pregel/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..90eb44b85492bf210333122b3f93bf26cefdd475
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/__init__.py
@@ -0,0 +1,3 @@
+from langgraph.pregel.main import NodeBuilder, Pregel
+
+__all__ = ("Pregel", "NodeBuilder")
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_algo.py b/langgraph/source/libs/langgraph/langgraph/pregel/_algo.py
new file mode 100644
index 0000000000000000000000000000000000000000..9f265f80ffb025f64e756fd0d95860a43525b0e7
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/_algo.py
@@ -0,0 +1,1233 @@
+from __future__ import annotations
+
+import binascii
+import itertools
+import sys
+import threading
+from collections import defaultdict, deque
+from collections.abc import Callable, Iterable, Mapping, Sequence
+from copy import copy
+from functools import partial
+from hashlib import sha1
+from typing import (
+ Any,
+ Literal,
+ NamedTuple,
+ Protocol,
+ cast,
+ overload,
+)
+
+from langchain_core.callbacks import Callbacks
+from langchain_core.callbacks.manager import AsyncParentRunManager, ParentRunManager
+from langchain_core.runnables.config import RunnableConfig
+from langgraph.checkpoint.base import (
+ BaseCheckpointSaver,
+ ChannelVersions,
+ Checkpoint,
+ PendingWrite,
+ V,
+)
+from langgraph.store.base import BaseStore
+from xxhash import xxh3_128_hexdigest
+
+from langgraph._internal._config import merge_configs, patch_config
+from langgraph._internal._constants import (
+ CACHE_NS_WRITES,
+ CONF,
+ CONFIG_KEY_CHECKPOINT_ID,
+ CONFIG_KEY_CHECKPOINT_MAP,
+ CONFIG_KEY_CHECKPOINT_NS,
+ CONFIG_KEY_CHECKPOINTER,
+ CONFIG_KEY_READ,
+ CONFIG_KEY_RESUME_MAP,
+ CONFIG_KEY_RUNTIME,
+ CONFIG_KEY_SCRATCHPAD,
+ CONFIG_KEY_SEND,
+ CONFIG_KEY_TASK_ID,
+ ERROR,
+ INTERRUPT,
+ NO_WRITES,
+ NS_END,
+ NS_SEP,
+ NULL_TASK_ID,
+ PREVIOUS,
+ PULL,
+ PUSH,
+ RESERVED,
+ RESUME,
+ RETURN,
+ TASKS,
+)
+from langgraph._internal._scratchpad import PregelScratchpad
+from langgraph._internal._typing import EMPTY_SEQ, MISSING
+from langgraph.channels.base import BaseChannel
+from langgraph.channels.topic import Topic
+from langgraph.channels.untracked_value import UntrackedValue
+from langgraph.constants import TAG_HIDDEN
+from langgraph.managed.base import ManagedValueMapping
+from langgraph.pregel._call import get_runnable_for_task, identifier
+from langgraph.pregel._io import read_channels
+from langgraph.pregel._log import logger
+from langgraph.pregel._read import INPUT_CACHE_KEY_TYPE, PregelNode
+from langgraph.runtime import DEFAULT_RUNTIME, Runtime
+from langgraph.types import (
+ All,
+ CacheKey,
+ CachePolicy,
+ PregelExecutableTask,
+ PregelTask,
+ RetryPolicy,
+ Send,
+)
+
+GetNextVersion = Callable[[V | None, None], V]
+SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11)
+
+
+class WritesProtocol(Protocol):
+ """Protocol for objects containing writes to be applied to checkpoint.
+ Implemented by PregelTaskWrites and PregelExecutableTask."""
+
+ @property
+ def path(self) -> tuple[str | int | tuple, ...]: ...
+
+ @property
+ def name(self) -> str: ...
+
+ @property
+ def writes(self) -> Sequence[tuple[str, Any]]: ...
+
+ @property
+ def triggers(self) -> Sequence[str]: ...
+
+
+class PregelTaskWrites(NamedTuple):
+ """Simplest implementation of WritesProtocol, for usage with writes that
+ don't originate from a runnable task, eg. graph input, update_state, etc."""
+
+ path: tuple[str | int | tuple, ...]
+ name: str
+ writes: Sequence[tuple[str, Any]]
+ triggers: Sequence[str]
+
+
+class Call:
+ __slots__ = ("func", "input", "retry_policy", "cache_policy", "callbacks")
+
+ func: Callable
+ input: tuple[tuple[Any, ...], dict[str, Any]]
+ retry_policy: Sequence[RetryPolicy] | None
+ cache_policy: CachePolicy | None
+ callbacks: Callbacks
+
+ def __init__(
+ self,
+ func: Callable,
+ input: tuple[tuple[Any, ...], dict[str, Any]],
+ *,
+ retry_policy: Sequence[RetryPolicy] | None,
+ cache_policy: CachePolicy | None,
+ callbacks: Callbacks,
+ ) -> None:
+ self.func = func
+ self.input = input
+ self.retry_policy = retry_policy
+ self.cache_policy = cache_policy
+ self.callbacks = callbacks
+
+
+def should_interrupt(
+ checkpoint: Checkpoint,
+ interrupt_nodes: All | Sequence[str],
+ tasks: Iterable[PregelExecutableTask],
+) -> list[PregelExecutableTask]:
+ """Check if the graph should be interrupted based on current state."""
+ version_type = type(next(iter(checkpoint["channel_versions"].values()), None))
+ null_version = version_type() # type: ignore[misc]
+ seen = checkpoint["versions_seen"].get(INTERRUPT, {})
+ # interrupt if any channel has been updated since last interrupt
+ any_updates_since_prev_interrupt = any(
+ version > seen.get(chan, null_version) # type: ignore[operator]
+ for chan, version in checkpoint["channel_versions"].items()
+ )
+ # and any triggered node is in interrupt_nodes list
+ return (
+ [
+ task
+ for task in tasks
+ if (
+ (
+ not task.config
+ or TAG_HIDDEN not in task.config.get("tags", EMPTY_SEQ)
+ )
+ if interrupt_nodes == "*"
+ else task.name in interrupt_nodes
+ )
+ ]
+ if any_updates_since_prev_interrupt
+ else []
+ )
+
+
+def local_read(
+ scratchpad: PregelScratchpad,
+ channels: Mapping[str, BaseChannel],
+ managed: ManagedValueMapping,
+ task: WritesProtocol,
+ select: list[str] | str,
+ fresh: bool = False,
+) -> dict[str, Any] | Any:
+ """Function injected under CONFIG_KEY_READ in task config, to read current state.
+ Used by conditional edges to read a copy of the state with reflecting the writes
+ from that node only."""
+ updated: dict[str, list[Any]] = defaultdict(list)
+ if isinstance(select, str):
+ managed_keys = []
+ for c, v in task.writes:
+ if c == select:
+ updated[c].append(v)
+ else:
+ managed_keys = [k for k in select if k in managed]
+ select = [k for k in select if k not in managed]
+ for c, v in task.writes:
+ if c in select:
+ updated[c].append(v)
+ if fresh:
+ # apply writes
+ local_channels: dict[str, BaseChannel] = {}
+ for k in channels:
+ cc = channels[k].copy()
+ cc.update(updated[k])
+ local_channels[k] = cc
+ # read fresh values
+ values = read_channels(local_channels, select)
+ else:
+ values = read_channels(channels, select)
+ if managed_keys:
+ values.update({k: managed[k].get(scratchpad) for k in managed_keys})
+ return values
+
+
+def increment(current: int | None, channel: None) -> int:
+ """Default channel versioning function, increments the current int version."""
+ return current + 1 if current is not None else 1
+
+
+def apply_writes(
+ checkpoint: Checkpoint,
+ channels: Mapping[str, BaseChannel],
+ tasks: Iterable[WritesProtocol],
+ get_next_version: GetNextVersion | None,
+ trigger_to_nodes: Mapping[str, Sequence[str]],
+) -> set[str]:
+ """Apply writes from a set of tasks (usually the tasks from a Pregel step)
+ to the checkpoint and channels, and return managed values writes to be applied
+ externally.
+
+ Args:
+ checkpoint: The checkpoint to update.
+ channels: The channels to update.
+ tasks: The tasks to apply writes from.
+ get_next_version: Optional function to determine the next version of a channel.
+ trigger_to_nodes: Mapping of channel names to the set of nodes that can be triggered by updates to that channel.
+
+ Returns:
+ Set of channels that were updated in this step.
+ """
+ # sort tasks on path, to ensure deterministic order for update application
+ # any path parts after the 3rd are ignored for sorting
+ # (we use them for eg. task ids which aren't good for sorting)
+ tasks = sorted(tasks, key=lambda t: task_path_str(t.path[:3]))
+ # if no task has triggers this is applying writes from the null task only
+ # so we don't do anything other than update the channels written to
+ bump_step = any(t.triggers for t in tasks)
+
+ # update seen versions
+ for task in tasks:
+ checkpoint["versions_seen"].setdefault(task.name, {}).update(
+ {
+ chan: checkpoint["channel_versions"][chan]
+ for chan in task.triggers
+ if chan in checkpoint["channel_versions"]
+ }
+ )
+
+ # Find the highest version of all channels
+ if get_next_version is None:
+ next_version = None
+ else:
+ next_version = get_next_version(
+ (
+ max(checkpoint["channel_versions"].values())
+ if checkpoint["channel_versions"]
+ else None
+ ),
+ None,
+ )
+
+ # Consume all channels that were read
+ for chan in {
+ chan
+ for task in tasks
+ for chan in task.triggers
+ if chan not in RESERVED and chan in channels
+ }:
+ if channels[chan].consume() and next_version is not None:
+ checkpoint["channel_versions"][chan] = next_version
+
+ # Group writes by channel
+ pending_writes_by_channel: dict[str, list[Any]] = defaultdict(list)
+ for task in tasks:
+ for chan, val in task.writes:
+ if chan in (NO_WRITES, PUSH, RESUME, INTERRUPT, RETURN, ERROR):
+ pass
+ elif chan in channels:
+ pending_writes_by_channel[chan].append(val)
+ else:
+ logger.warning(
+ f"Task {task.name} with path {task.path} wrote to unknown channel {chan}, ignoring it."
+ )
+
+ # Apply writes to channels
+ updated_channels: set[str] = set()
+ for chan, vals in pending_writes_by_channel.items():
+ if chan in channels:
+ if channels[chan].update(vals) and next_version is not None:
+ checkpoint["channel_versions"][chan] = next_version
+ # unavailable channels can't trigger tasks, so don't add them
+ if channels[chan].is_available():
+ updated_channels.add(chan)
+
+ # Channels that weren't updated in this step are notified of a new step
+ if bump_step:
+ for chan in channels:
+ if channels[chan].is_available() and chan not in updated_channels:
+ if channels[chan].update(EMPTY_SEQ) and next_version is not None:
+ checkpoint["channel_versions"][chan] = next_version
+ # unavailable channels can't trigger tasks, so don't add them
+ if channels[chan].is_available():
+ updated_channels.add(chan)
+
+ # If this is (tentatively) the last superstep, notify all channels of finish
+ if bump_step and updated_channels.isdisjoint(trigger_to_nodes):
+ for chan in channels:
+ if channels[chan].finish() and next_version is not None:
+ checkpoint["channel_versions"][chan] = next_version
+ # unavailable channels can't trigger tasks, so don't add them
+ if channels[chan].is_available():
+ updated_channels.add(chan)
+
+ # Return managed values writes to be applied externally
+ return updated_channels
+
+
+@overload
+def prepare_next_tasks(
+ checkpoint: Checkpoint,
+ pending_writes: list[PendingWrite],
+ processes: Mapping[str, PregelNode],
+ channels: Mapping[str, BaseChannel],
+ managed: ManagedValueMapping,
+ config: RunnableConfig,
+ step: int,
+ stop: int,
+ *,
+ for_execution: Literal[False],
+ store: Literal[None] = None,
+ checkpointer: Literal[None] = None,
+ manager: Literal[None] = None,
+ trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
+ updated_channels: set[str] | None = None,
+ retry_policy: Sequence[RetryPolicy] = (),
+ cache_policy: Literal[None] = None,
+) -> dict[str, PregelTask]: ...
+
+
+@overload
+def prepare_next_tasks(
+ checkpoint: Checkpoint,
+ pending_writes: list[PendingWrite],
+ processes: Mapping[str, PregelNode],
+ channels: Mapping[str, BaseChannel],
+ managed: ManagedValueMapping,
+ config: RunnableConfig,
+ step: int,
+ stop: int,
+ *,
+ for_execution: Literal[True],
+ store: BaseStore | None,
+ checkpointer: BaseCheckpointSaver | None,
+ manager: None | ParentRunManager | AsyncParentRunManager,
+ trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
+ updated_channels: set[str] | None = None,
+ retry_policy: Sequence[RetryPolicy] = (),
+ cache_policy: CachePolicy | None = None,
+) -> dict[str, PregelExecutableTask]: ...
+
+
+def prepare_next_tasks(
+ checkpoint: Checkpoint,
+ pending_writes: list[PendingWrite],
+ processes: Mapping[str, PregelNode],
+ channels: Mapping[str, BaseChannel],
+ managed: ManagedValueMapping,
+ config: RunnableConfig,
+ step: int,
+ stop: int,
+ *,
+ for_execution: bool,
+ store: BaseStore | None = None,
+ checkpointer: BaseCheckpointSaver | None = None,
+ manager: None | ParentRunManager | AsyncParentRunManager = None,
+ trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
+ updated_channels: set[str] | None = None,
+ retry_policy: Sequence[RetryPolicy] = (),
+ cache_policy: CachePolicy | None = None,
+) -> dict[str, PregelTask] | dict[str, PregelExecutableTask]:
+ """Prepare the set of tasks that will make up the next Pregel step.
+
+ Args:
+ checkpoint: The current checkpoint.
+ pending_writes: The list of pending writes.
+ processes: The mapping of process names to PregelNode instances.
+ channels: The mapping of channel names to BaseChannel instances.
+ managed: The mapping of managed value names to functions.
+ config: The `Runnable` configuration.
+ step: The current step.
+ for_execution: Whether the tasks are being prepared for execution.
+ store: An instance of BaseStore to make it available for usage within tasks.
+ checkpointer: `Checkpointer` instance used for saving checkpoints.
+ manager: The parent run manager to use for the tasks.
+ trigger_to_nodes: Optional: Mapping of channel names to the set of nodes
+ that are can be triggered by that channel.
+ updated_channels: Optional. Set of channel names that have been updated during
+ the previous step. Using in conjunction with trigger_to_nodes to speed
+ up the process of determining which nodes should be triggered in the next
+ step.
+
+ Returns:
+ A dictionary of tasks to be executed. The keys are the task ids and the values
+ are the tasks themselves. This is the union of all PUSH tasks (Sends)
+ and PULL tasks (nodes triggered by edges).
+ """
+ input_cache: dict[INPUT_CACHE_KEY_TYPE, Any] = {}
+ checkpoint_id_bytes = binascii.unhexlify(checkpoint["id"].replace("-", ""))
+ null_version = checkpoint_null_version(checkpoint)
+ tasks: list[PregelTask | PregelExecutableTask] = []
+ # Consume pending tasks
+ tasks_channel = cast(Topic[Send] | None, channels.get(TASKS))
+ if tasks_channel and tasks_channel.is_available():
+ for idx, _ in enumerate(tasks_channel.get()):
+ if task := prepare_single_task(
+ (PUSH, idx),
+ None,
+ checkpoint=checkpoint,
+ checkpoint_id_bytes=checkpoint_id_bytes,
+ checkpoint_null_version=null_version,
+ pending_writes=pending_writes,
+ processes=processes,
+ channels=channels,
+ managed=managed,
+ config=config,
+ step=step,
+ stop=stop,
+ for_execution=for_execution,
+ store=store,
+ checkpointer=checkpointer,
+ manager=manager,
+ input_cache=input_cache,
+ cache_policy=cache_policy,
+ retry_policy=retry_policy,
+ ):
+ tasks.append(task)
+
+ # This section is an optimization that allows which nodes will be active
+ # during the next step.
+ # When there's information about:
+ # 1. Which channels were updated in the previous step
+ # 2. Which nodes are triggered by which channels
+ # Then we can determine which nodes should be triggered in the next step
+ # without having to cycle through all nodes.
+ if updated_channels and trigger_to_nodes:
+ triggered_nodes: set[str] = set()
+ # Get all nodes that have triggers associated with an updated channel
+ for channel in updated_channels:
+ if node_ids := trigger_to_nodes.get(channel):
+ triggered_nodes.update(node_ids)
+ # Sort the nodes to ensure deterministic order
+ candidate_nodes: Iterable[str] = sorted(triggered_nodes)
+ elif not checkpoint["channel_versions"]:
+ candidate_nodes = ()
+ else:
+ candidate_nodes = processes.keys()
+
+ # Check if any processes should be run in next step
+ # If so, prepare the values to be passed to them
+ for name in candidate_nodes:
+ if task := prepare_single_task(
+ (PULL, name),
+ None,
+ checkpoint=checkpoint,
+ checkpoint_id_bytes=checkpoint_id_bytes,
+ checkpoint_null_version=null_version,
+ pending_writes=pending_writes,
+ processes=processes,
+ channels=channels,
+ managed=managed,
+ config=config,
+ step=step,
+ stop=stop,
+ for_execution=for_execution,
+ store=store,
+ checkpointer=checkpointer,
+ manager=manager,
+ input_cache=input_cache,
+ cache_policy=cache_policy,
+ retry_policy=retry_policy,
+ ):
+ tasks.append(task)
+ return {t.id: t for t in tasks}
+
+
+PUSH_TRIGGER = (PUSH,)
+
+
+class _TaskIDFn(Protocol):
+ def __call__(self, namespace: bytes, *parts: str | bytes) -> str:
+ pass
+
+
+def prepare_single_task(
+ task_path: tuple[Any, ...],
+ task_id_checksum: str | None,
+ *,
+ checkpoint: Checkpoint,
+ checkpoint_id_bytes: bytes,
+ checkpoint_null_version: V | None,
+ pending_writes: list[PendingWrite],
+ processes: Mapping[str, PregelNode],
+ channels: Mapping[str, BaseChannel],
+ managed: ManagedValueMapping,
+ config: RunnableConfig,
+ step: int,
+ stop: int,
+ for_execution: bool,
+ store: BaseStore | None = None,
+ checkpointer: BaseCheckpointSaver | None = None,
+ manager: None | ParentRunManager | AsyncParentRunManager = None,
+ input_cache: dict[INPUT_CACHE_KEY_TYPE, Any] | None = None,
+ cache_policy: CachePolicy | None = None,
+ retry_policy: Sequence[RetryPolicy] = (),
+) -> None | PregelTask | PregelExecutableTask:
+ """Prepares a single task for the next Pregel step, given a task path, which
+ uniquely identifies a PUSH or PULL task within the graph."""
+ configurable = config.get(CONF, {})
+ parent_ns = configurable.get(CONFIG_KEY_CHECKPOINT_NS, "")
+ task_id_func = _xxhash_str if checkpoint["v"] > 1 else _uuid5_str
+
+ if task_path[0] == PUSH and isinstance(task_path[-1], Call):
+ return prepare_push_task_functional(
+ cast(tuple[str, tuple, int, str, Call], task_path),
+ task_id_checksum,
+ checkpoint=checkpoint,
+ checkpoint_id_bytes=checkpoint_id_bytes,
+ pending_writes=pending_writes,
+ channels=channels,
+ managed=managed,
+ config=config,
+ step=step,
+ stop=stop,
+ for_execution=for_execution,
+ store=store,
+ checkpointer=checkpointer,
+ manager=manager,
+ cache_policy=cache_policy,
+ retry_policy=retry_policy,
+ parent_ns=parent_ns,
+ task_id_func=task_id_func,
+ )
+
+ elif task_path[0] == PUSH:
+ return prepare_push_task_send(
+ cast(tuple[str, tuple], task_path),
+ task_id_checksum,
+ checkpoint=checkpoint,
+ checkpoint_id_bytes=checkpoint_id_bytes,
+ pending_writes=pending_writes,
+ channels=channels,
+ managed=managed,
+ config=config,
+ step=step,
+ processes=processes,
+ stop=stop,
+ for_execution=for_execution,
+ store=store,
+ checkpointer=checkpointer,
+ manager=manager,
+ cache_policy=cache_policy,
+ retry_policy=retry_policy,
+ parent_ns=parent_ns,
+ task_id_func=task_id_func,
+ )
+
+ elif task_path[0] == PULL:
+ # (PULL, node name)
+ name = cast(str, task_path[1])
+ if name not in processes:
+ return
+ proc = processes[name]
+ if checkpoint_null_version is None:
+ return
+ # If any of the channels read by this process were updated
+ if _triggers(
+ channels,
+ checkpoint["channel_versions"],
+ checkpoint["versions_seen"].get(name),
+ checkpoint_null_version,
+ proc,
+ ):
+ triggers = tuple(sorted(proc.triggers))
+ # create task id
+ checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
+ task_id = task_id_func(
+ checkpoint_id_bytes,
+ checkpoint_ns,
+ str(step),
+ name,
+ PULL,
+ *triggers,
+ )
+ task_checkpoint_ns = f"{checkpoint_ns}{NS_END}{task_id}"
+ # create scratchpad
+ scratchpad = _scratchpad(
+ config[CONF].get(CONFIG_KEY_SCRATCHPAD),
+ pending_writes,
+ task_id,
+ xxh3_128_hexdigest(task_checkpoint_ns.encode()),
+ config[CONF].get(CONFIG_KEY_RESUME_MAP),
+ step,
+ stop,
+ )
+ # create task input
+ try:
+ val = _proc_input(
+ proc,
+ managed,
+ channels,
+ for_execution=for_execution,
+ input_cache=input_cache,
+ scratchpad=scratchpad,
+ )
+ if val is MISSING:
+ return
+ except Exception as exc:
+ if SUPPORTS_EXC_NOTES:
+ exc.add_note(
+ f"Before task with name '{name}' and path '{task_path[:3]}'"
+ )
+ raise
+
+ metadata = {
+ "langgraph_step": step,
+ "langgraph_node": name,
+ "langgraph_triggers": triggers,
+ "langgraph_path": task_path[:3],
+ "langgraph_checkpoint_ns": task_checkpoint_ns,
+ }
+ if task_id_checksum is not None:
+ assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
+ if for_execution:
+ if node := proc.node:
+ if proc.metadata:
+ metadata.update(proc.metadata)
+ writes: deque[tuple[str, Any]] = deque()
+ cache_policy = proc.cache_policy or cache_policy
+ if cache_policy:
+ args_key = cache_policy.key_func(val)
+ cache_key = CacheKey(
+ (
+ CACHE_NS_WRITES,
+ (identifier(proc) or "__dynamic__"),
+ name,
+ ),
+ xxh3_128_hexdigest(
+ (
+ args_key.encode()
+ if isinstance(args_key, str)
+ else args_key
+ ),
+ ),
+ cache_policy.ttl,
+ )
+ else:
+ cache_key = None
+ runtime = cast(
+ Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME)
+ )
+ runtime = runtime.override(
+ previous=checkpoint["channel_values"].get(PREVIOUS, None),
+ store=store,
+ )
+ additional_config = {
+ "metadata": metadata,
+ "tags": proc.tags,
+ }
+ return PregelExecutableTask(
+ name,
+ val,
+ node,
+ writes,
+ patch_config(
+ merge_configs(
+ config, cast(RunnableConfig, additional_config)
+ ),
+ run_name=name,
+ callbacks=(
+ manager.get_child(f"graph:step:{step}")
+ if manager
+ else None
+ ),
+ configurable={
+ CONFIG_KEY_TASK_ID: task_id,
+ # deque.extend is thread-safe
+ CONFIG_KEY_SEND: writes.extend,
+ CONFIG_KEY_READ: partial(
+ local_read,
+ scratchpad,
+ channels,
+ managed,
+ PregelTaskWrites(
+ task_path[:3],
+ name,
+ writes,
+ triggers,
+ ),
+ ),
+ CONFIG_KEY_CHECKPOINTER: (
+ checkpointer
+ or configurable.get(CONFIG_KEY_CHECKPOINTER)
+ ),
+ CONFIG_KEY_CHECKPOINT_MAP: {
+ **configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
+ parent_ns: checkpoint["id"],
+ },
+ CONFIG_KEY_CHECKPOINT_ID: None,
+ CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
+ CONFIG_KEY_SCRATCHPAD: scratchpad,
+ CONFIG_KEY_RUNTIME: runtime,
+ },
+ ),
+ triggers,
+ proc.retry_policy or retry_policy,
+ cache_key,
+ task_id,
+ task_path[:3],
+ writers=proc.flat_writers,
+ subgraphs=proc.subgraphs,
+ )
+ else:
+ return PregelTask(task_id, name, task_path[:3])
+
+
+def prepare_push_task_functional(
+ task_path: tuple[str, tuple, int, str, Call],
+ # (PUSH, parent task path, idx of PUSH write, id of parent task, Call)
+ task_id_checksum: str | None,
+ *,
+ checkpoint: Checkpoint,
+ checkpoint_id_bytes: bytes,
+ pending_writes: list[PendingWrite],
+ channels: Mapping[str, BaseChannel],
+ managed: ManagedValueMapping,
+ config: RunnableConfig,
+ step: int,
+ stop: int,
+ for_execution: bool,
+ store: BaseStore | None = None,
+ checkpointer: BaseCheckpointSaver | None = None,
+ manager: None | ParentRunManager | AsyncParentRunManager = None,
+ cache_policy: CachePolicy | None = None,
+ retry_policy: Sequence[RetryPolicy] = (),
+ parent_ns: str,
+ # namespace: bytes, *parts: str | bytes
+ task_id_func: _TaskIDFn,
+) -> PregelTask | PregelExecutableTask:
+ """Prepare a push task with an attached caller. Used for the functional API."""
+ configurable = config.get(CONF, {})
+
+ call = task_path[-1]
+ proc_ = get_runnable_for_task(call.func)
+ name = proc_.name
+ if name is None:
+ raise ValueError("`call` functions must have a `__name__` attribute")
+ # create task id
+ triggers: Sequence[str] = PUSH_TRIGGER
+ checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
+ task_id = task_id_func(
+ checkpoint_id_bytes,
+ checkpoint_ns,
+ str(step),
+ name,
+ PUSH,
+ task_path_str(task_path[1]),
+ str(task_path[2]),
+ )
+ task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
+ # we append True to the task path to indicate that a call is being
+ # made, so we should not return interrupts from this task (responsibility lies with the parent)
+ in_progress_task_path = (*task_path[:3], True)
+ metadata = {
+ "langgraph_step": step,
+ "langgraph_node": name,
+ "langgraph_triggers": triggers,
+ "langgraph_path": in_progress_task_path,
+ "langgraph_checkpoint_ns": task_checkpoint_ns,
+ }
+ if task_id_checksum is not None:
+ assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
+ if for_execution:
+ writes: deque[tuple[str, Any]] = deque()
+ cache_policy = call.cache_policy or cache_policy
+ if cache_policy:
+ args_key = cache_policy.key_func(*call.input[0], **call.input[1])
+ cache_key: CacheKey | None = CacheKey(
+ (
+ CACHE_NS_WRITES,
+ (identifier(call.func) or "__dynamic__"),
+ ),
+ xxh3_128_hexdigest(
+ args_key.encode() if isinstance(args_key, str) else args_key,
+ ),
+ cache_policy.ttl,
+ )
+ else:
+ cache_key = None
+ scratchpad = _scratchpad(
+ configurable.get(CONFIG_KEY_SCRATCHPAD),
+ pending_writes,
+ task_id,
+ xxh3_128_hexdigest(task_checkpoint_ns.encode()),
+ configurable.get(CONFIG_KEY_RESUME_MAP),
+ step,
+ stop,
+ )
+ runtime = cast(Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME))
+ runtime = runtime.override(store=store)
+ return PregelExecutableTask(
+ name,
+ call.input,
+ proc_,
+ writes,
+ patch_config(
+ merge_configs(config, {"metadata": metadata}),
+ run_name=name,
+ callbacks=call.callbacks
+ or (manager.get_child(f"graph:step:{step}") if manager else None),
+ configurable={
+ CONFIG_KEY_TASK_ID: task_id,
+ # deque.extend is thread-safe
+ CONFIG_KEY_SEND: writes.extend,
+ CONFIG_KEY_READ: partial(
+ local_read,
+ scratchpad,
+ channels,
+ managed,
+ PregelTaskWrites(in_progress_task_path, name, writes, triggers),
+ ),
+ CONFIG_KEY_CHECKPOINTER: (
+ checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
+ ),
+ CONFIG_KEY_CHECKPOINT_MAP: {
+ **configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
+ parent_ns: checkpoint["id"],
+ },
+ CONFIG_KEY_CHECKPOINT_ID: None,
+ CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
+ CONFIG_KEY_SCRATCHPAD: scratchpad,
+ CONFIG_KEY_RUNTIME: runtime,
+ },
+ ),
+ triggers,
+ call.retry_policy or retry_policy,
+ cache_key,
+ task_id,
+ in_progress_task_path,
+ )
+ else:
+ return PregelTask(task_id, name, in_progress_task_path)
+
+
+def prepare_push_task_send(
+ task_path: tuple[str, tuple],
+ # (PUSH, parent task path)
+ task_id_checksum: str | None,
+ *,
+ checkpoint: Checkpoint,
+ checkpoint_id_bytes: bytes,
+ pending_writes: list[PendingWrite],
+ channels: Mapping[str, BaseChannel],
+ managed: ManagedValueMapping,
+ config: RunnableConfig,
+ step: int,
+ stop: int,
+ for_execution: bool,
+ store: BaseStore | None = None,
+ checkpointer: BaseCheckpointSaver | None = None,
+ manager: None | ParentRunManager | AsyncParentRunManager = None,
+ cache_policy: CachePolicy | None = None,
+ retry_policy: Sequence[RetryPolicy] = (),
+ parent_ns: str,
+ task_id_func: _TaskIDFn,
+ processes: Mapping[str, PregelNode],
+) -> PregelTask | PregelExecutableTask | None:
+ if len(task_path) == 2:
+ # SEND tasks, executed in superstep n+1
+ # (PUSH, idx of pending send)
+ idx = cast(int, task_path[1])
+ if not channels[TASKS].is_available():
+ return
+ sends: Sequence[Send] = channels[TASKS].get()
+ if idx < 0 or idx >= len(sends):
+ return
+ packet = sends[idx]
+ if not isinstance(packet, Send):
+ logger.warning(
+ f"Ignoring invalid packet type {type(packet)} in pending sends"
+ )
+ return
+
+ if packet.node not in processes:
+ logger.warning(f"Ignoring unknown node name {packet.node} in pending sends")
+ return
+ # find process
+ proc = processes[packet.node]
+ proc_node = proc.node
+ if proc_node is None:
+ return
+ # create task id
+ triggers = PUSH_TRIGGER
+ checkpoint_ns = (
+ f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
+ )
+ task_id = task_id_func(
+ checkpoint_id_bytes,
+ checkpoint_ns,
+ str(step),
+ packet.node,
+ PUSH,
+ str(idx),
+ )
+ else:
+ logger.warning(f"Ignoring invalid PUSH task path {task_path}")
+ return
+ configurable = config.get(CONF, {})
+ task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
+ # we append False to the task path to indicate that a call is not being made
+ # so we should return interrupts from this task
+ translated_task_path = (*task_path[:3], False)
+ metadata = {
+ "langgraph_step": step,
+ "langgraph_node": packet.node,
+ "langgraph_triggers": triggers,
+ "langgraph_path": translated_task_path,
+ "langgraph_checkpoint_ns": task_checkpoint_ns,
+ }
+ if task_id_checksum is not None:
+ assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
+ if for_execution:
+ if proc.metadata:
+ metadata.update(proc.metadata)
+ writes: deque[tuple[str, Any]] = deque()
+ cache_policy = proc.cache_policy or cache_policy
+ if cache_policy:
+ args_key = cache_policy.key_func(packet.arg)
+ cache_key = CacheKey(
+ (
+ CACHE_NS_WRITES,
+ (identifier(proc) or "__dynamic__"),
+ packet.node,
+ ),
+ xxh3_128_hexdigest(
+ args_key.encode() if isinstance(args_key, str) else args_key,
+ ),
+ cache_policy.ttl,
+ )
+ else:
+ cache_key = None
+ scratchpad = _scratchpad(
+ config[CONF].get(CONFIG_KEY_SCRATCHPAD),
+ pending_writes,
+ task_id,
+ xxh3_128_hexdigest(task_checkpoint_ns.encode()),
+ config[CONF].get(CONFIG_KEY_RESUME_MAP),
+ step,
+ stop,
+ )
+ runtime = cast(Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME))
+ runtime = runtime.override(
+ store=store, previous=checkpoint["channel_values"].get(PREVIOUS, None)
+ )
+ additional_config: RunnableConfig = {
+ "metadata": metadata,
+ "tags": proc.tags,
+ }
+ return PregelExecutableTask(
+ packet.node,
+ packet.arg,
+ proc_node,
+ writes,
+ patch_config(
+ merge_configs(config, additional_config),
+ run_name=packet.node,
+ callbacks=(
+ manager.get_child(f"graph:step:{step}") if manager else None
+ ),
+ configurable={
+ CONFIG_KEY_TASK_ID: task_id,
+ # deque.extend is thread-safe
+ CONFIG_KEY_SEND: writes.extend,
+ CONFIG_KEY_READ: partial(
+ local_read,
+ scratchpad,
+ channels,
+ managed,
+ PregelTaskWrites(
+ translated_task_path, packet.node, writes, triggers
+ ),
+ ),
+ CONFIG_KEY_CHECKPOINTER: (
+ checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
+ ),
+ CONFIG_KEY_CHECKPOINT_MAP: {
+ **configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
+ parent_ns: checkpoint["id"],
+ },
+ CONFIG_KEY_CHECKPOINT_ID: None,
+ CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
+ CONFIG_KEY_SCRATCHPAD: scratchpad,
+ CONFIG_KEY_RUNTIME: runtime,
+ },
+ ),
+ triggers,
+ proc.retry_policy or retry_policy,
+ cache_key,
+ task_id,
+ translated_task_path,
+ writers=proc.flat_writers,
+ subgraphs=proc.subgraphs,
+ )
+ else:
+ return PregelTask(task_id, packet.node, translated_task_path)
+
+
+def checkpoint_null_version(
+ checkpoint: Checkpoint,
+) -> V | None:
+ """Get the null version for the checkpoint, if available."""
+ for version in checkpoint["channel_versions"].values():
+ return type(version)()
+ return None
+
+
+def _triggers(
+ channels: Mapping[str, BaseChannel],
+ versions: ChannelVersions,
+ seen: ChannelVersions | None,
+ null_version: V,
+ proc: PregelNode,
+) -> bool:
+ if seen is None:
+ for chan in proc.triggers:
+ if channels[chan].is_available():
+ return True
+ else:
+ for chan in proc.triggers:
+ if channels[chan].is_available() and versions.get( # type: ignore[operator]
+ chan, null_version
+ ) > seen.get(chan, null_version):
+ return True
+ return False
+
+
+def _scratchpad(
+ parent_scratchpad: PregelScratchpad | None,
+ pending_writes: list[PendingWrite],
+ task_id: str,
+ namespace_hash: str,
+ resume_map: dict[str, Any] | None,
+ step: int,
+ stop: int,
+) -> PregelScratchpad:
+ if len(pending_writes) > 0:
+ # find global resume value
+ for w in pending_writes:
+ if w[0] == NULL_TASK_ID and w[1] == RESUME:
+ null_resume_write = w
+ break
+ else:
+ # None cannot be used as a resume value, because it would be difficult to
+ # distinguish from missing when used over http
+ null_resume_write = None
+
+ # find task-specific resume value
+ for w in pending_writes:
+ if w[0] == task_id and w[1] == RESUME:
+ task_resume_write = w[2]
+ if not isinstance(task_resume_write, list):
+ task_resume_write = [task_resume_write]
+ break
+ else:
+ task_resume_write = []
+ del w
+
+ # find namespace and task-specific resume value
+ if resume_map and namespace_hash in resume_map:
+ mapped_resume_write = resume_map[namespace_hash]
+ task_resume_write.append(mapped_resume_write)
+
+ else:
+ null_resume_write = None
+ task_resume_write = []
+
+ def get_null_resume(consume: bool = False) -> Any:
+ if null_resume_write is None:
+ if parent_scratchpad is not None:
+ return parent_scratchpad.get_null_resume(consume)
+ return None
+ if consume:
+ try:
+ pending_writes.remove(null_resume_write)
+ return null_resume_write[2]
+ except ValueError:
+ return None
+ return null_resume_write[2]
+
+ # using itertools.count as an atomic counter (+= 1 is not thread-safe)
+ return PregelScratchpad(
+ step=step,
+ stop=stop,
+ # call
+ call_counter=LazyAtomicCounter(),
+ # interrupt
+ interrupt_counter=LazyAtomicCounter(),
+ resume=task_resume_write,
+ get_null_resume=get_null_resume,
+ # subgraph
+ subgraph_counter=LazyAtomicCounter(),
+ )
+
+
+def _proc_input(
+ proc: PregelNode,
+ managed: ManagedValueMapping,
+ channels: Mapping[str, BaseChannel],
+ *,
+ for_execution: bool,
+ scratchpad: PregelScratchpad,
+ input_cache: dict[INPUT_CACHE_KEY_TYPE, Any] | None,
+) -> Any:
+ """Prepare input for a PULL task, based on the process's channels and triggers."""
+ # if in cache return shallow copy
+ if input_cache is not None and proc.input_cache_key in input_cache:
+ return copy(input_cache[proc.input_cache_key])
+ # If all trigger channels subscribed by this process are not empty
+ # then invoke the process with the values of all non-empty channels
+ if isinstance(proc.channels, list):
+ val: dict[str, Any] = {}
+ for chan in proc.channels:
+ if chan in channels:
+ if channels[chan].is_available():
+ val[chan] = channels[chan].get()
+ else:
+ val[chan] = managed[chan].get(scratchpad)
+ elif isinstance(proc.channels, str):
+ if proc.channels in channels:
+ if channels[proc.channels].is_available():
+ val = channels[proc.channels].get()
+ else:
+ return MISSING
+ else:
+ return MISSING
+ else:
+ raise RuntimeError(
+ f"Invalid channels type, expected list or dict, got {proc.channels}"
+ )
+
+ # If the process has a mapper, apply it to the value
+ if for_execution and proc.mapper is not None:
+ val = proc.mapper(val)
+
+ # Cache the input value
+ if input_cache is not None:
+ input_cache[proc.input_cache_key] = val
+
+ return val
+
+
+def _uuid5_str(namespace: bytes, *parts: str | bytes) -> str:
+ """Generate a UUID from the SHA-1 hash of a namespace and str parts."""
+
+ sha = sha1(namespace, usedforsecurity=False)
+ sha.update(b"".join(p.encode() if isinstance(p, str) else p for p in parts))
+ hex = sha.hexdigest()
+ return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}"
+
+
+def _xxhash_str(namespace: bytes, *parts: str | bytes) -> str:
+ """Generate a UUID from the XXH3 hash of a namespace and str parts."""
+ hex = xxh3_128_hexdigest(
+ namespace + b"".join(p.encode() if isinstance(p, str) else p for p in parts)
+ )
+ return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}"
+
+
+def task_path_str(tup: str | int | tuple) -> str:
+ """Generate a string representation of the task path."""
+ return (
+ f"~{', '.join(task_path_str(x) for x in tup)}"
+ if isinstance(tup, (tuple, list))
+ else f"{tup:010d}"
+ if isinstance(tup, int)
+ else str(tup)
+ )
+
+
+LAZY_ATOMIC_COUNTER_LOCK = threading.Lock()
+
+
+class LazyAtomicCounter:
+ __slots__ = ("_counter",)
+
+ _counter: Callable[[], int] | None
+
+ def __init__(self) -> None:
+ self._counter = None
+
+ def __call__(self) -> int:
+ if self._counter is None:
+ with LAZY_ATOMIC_COUNTER_LOCK:
+ if self._counter is None:
+ self._counter = itertools.count(0).__next__
+ return self._counter()
+
+
+def sanitize_untracked_values_in_send(
+ packet: Send, channels: Mapping[str, BaseChannel]
+) -> Send:
+ """Pop any values belonging to UntrackedValue channels in Send.arg for safe checkpointing.
+
+ Send is often called with state to be passed to the dest node, which may contain
+ UntrackedValues at the top level. Send is not typed and arg may be a nested dict."""
+
+ if not isinstance(packet.arg, dict):
+ # Command
+ return packet
+
+ # top level keys should be the channel names
+ sanitized_arg = {
+ k: v
+ for k, v in packet.arg.items()
+ if not isinstance(channels.get(k), UntrackedValue)
+ }
+ return Send(node=packet.node, arg=sanitized_arg)
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_call.py b/langgraph/source/libs/langgraph/langgraph/pregel/_call.py
new file mode 100644
index 0000000000000000000000000000000000000000..0cd007042b9c00b9e1372a5ad79c8b53e508051f
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/_call.py
@@ -0,0 +1,269 @@
+"""Utility to convert a user provided function into a Runnable with a ChannelWrite."""
+
+from __future__ import annotations
+
+import concurrent.futures
+import functools
+import inspect
+import sys
+import types
+from collections.abc import Awaitable, Callable, Generator, Sequence
+from typing import Any, Generic, TypeVar, cast
+
+from langchain_core.runnables import Runnable
+from typing_extensions import ParamSpec
+
+from langgraph._internal._constants import CONF, CONFIG_KEY_CALL, RETURN
+from langgraph._internal._runnable import (
+ RunnableCallable,
+ RunnableSeq,
+ is_async_callable,
+ run_in_executor,
+)
+from langgraph.config import get_config
+from langgraph.pregel._write import ChannelWrite, ChannelWriteEntry
+from langgraph.types import CachePolicy, RetryPolicy
+
+##
+# Utilities borrowed from cloudpickle.
+# https://github.com/cloudpipe/cloudpickle/blob/6220b0ce83ffee5e47e06770a1ee38ca9e47c850/cloudpickle/cloudpickle.py#L265
+
+
+def _getattribute(obj: Any, name: str) -> Any:
+ parent = None
+ for subpath in name.split("."):
+ if subpath == "":
+ raise AttributeError(f"Can't get local attribute {name!r} on {obj!r}")
+ try:
+ parent = obj
+ obj = getattr(obj, subpath)
+ except AttributeError:
+ raise AttributeError(f"Can't get attribute {name!r} on {obj!r}") from None
+ return obj, parent
+
+
+def _whichmodule(obj: Any, name: str) -> str | None:
+ """Find the module an object belongs to.
+
+ This function differs from ``pickle.whichmodule`` in two ways:
+ - it does not mangle the cases where obj's module is __main__ and obj was
+ not found in any module.
+ - Errors arising during module introspection are ignored, as those errors
+ are considered unwanted side effects.
+ """
+ module_name = getattr(obj, "__module__", None)
+
+ if module_name is not None:
+ return module_name
+ # Protect the iteration by using a copy of sys.modules against dynamic
+ # modules that trigger imports of other modules upon calls to getattr or
+ # other threads importing at the same time.
+ for module_name, module in sys.modules.copy().items():
+ # Some modules such as coverage can inject non-module objects inside
+ # sys.modules
+ if (
+ module_name == "__main__"
+ or module_name == "__mp_main__"
+ or module is None
+ or not isinstance(module, types.ModuleType)
+ ):
+ continue
+ try:
+ if _getattribute(module, name)[0] is obj:
+ return module_name
+ except Exception:
+ pass
+ return None
+
+
+def identifier(obj: Any, name: str | None = None) -> str | None:
+ """Return the module and name of an object."""
+ from langgraph._internal._runnable import RunnableCallable, RunnableSeq
+ from langgraph.pregel._read import PregelNode
+
+ if isinstance(obj, PregelNode):
+ obj = obj.bound
+ if isinstance(obj, RunnableSeq):
+ obj = obj.steps[0]
+ if isinstance(obj, RunnableCallable):
+ obj = obj.func
+ if name is None:
+ name = getattr(obj, "__qualname__", None)
+ if name is None: # pragma: no cover
+ # This used to be needed for Python 2.7 support but is probably not
+ # needed anymore. However we keep the __name__ introspection in case
+ # users of cloudpickle rely on this old behavior for unknown reasons.
+ name = getattr(obj, "__name__", None)
+ if name is None:
+ return None
+
+ module_name = getattr(obj, "__module__", None)
+ if module_name is None:
+ # In this case, obj.__module__ is None. obj is thus treated as dynamic.
+ return None
+
+ return f"{module_name}.{name}"
+
+
+def _lookup_module_and_qualname(
+ obj: Any, name: str | None = None
+) -> tuple[types.ModuleType, str] | None:
+ if name is None:
+ name = getattr(obj, "__qualname__", None)
+ if name is None: # pragma: no cover
+ # This used to be needed for Python 2.7 support but is probably not
+ # needed anymore. However we keep the __name__ introspection in case
+ # users of cloudpickle rely on this old behavior for unknown reasons.
+ name = getattr(obj, "__name__", None)
+ if name is None:
+ return None
+
+ module_name = _whichmodule(obj, name)
+
+ if module_name is None:
+ # In this case, obj.__module__ is None AND obj was not found in any
+ # imported module. obj is thus treated as dynamic.
+ return None
+
+ if module_name == "__main__":
+ return None
+
+ # Note: if module_name is in sys.modules, the corresponding module is
+ # assumed importable at unpickling time. See #357
+ module = sys.modules.get(module_name, None)
+ if module is None:
+ # The main reason why obj's module would not be imported is that this
+ # module has been dynamically created, using for example
+ # types.ModuleType. The other possibility is that module was removed
+ # from sys.modules after obj was created/imported. But this case is not
+ # supported, as the standard pickle does not support it either.
+ return None
+
+ try:
+ obj2, parent = _getattribute(module, name)
+ except AttributeError:
+ # obj was not found inside the module it points to
+ return None
+ if obj2 is not obj:
+ return None
+ return module, name
+
+
+def _explode_args_trace_inputs(
+ sig: inspect.Signature, input: tuple[tuple[Any, ...], dict[str, Any]]
+) -> dict[str, Any]:
+ args, kwargs = input
+ bound = sig.bind_partial(*args, **kwargs)
+ bound.apply_defaults()
+ arguments = dict(bound.arguments)
+ arguments.pop("self", None)
+ arguments.pop("cls", None)
+ for param_name, param in sig.parameters.items():
+ if param.kind == inspect.Parameter.VAR_KEYWORD:
+ # Update with the **kwargs, and remove the original entry
+ # This is to help flatten out keyword arguments
+ if param_name in arguments:
+ arguments.update(arguments.pop(param_name))
+ return arguments
+
+
+def get_runnable_for_entrypoint(func: Callable[..., Any]) -> Runnable:
+ key = (func, False)
+ if key in CACHE:
+ return CACHE[key]
+ else:
+ if is_async_callable(func):
+ run = RunnableCallable(
+ None, func, name=func.__name__, trace=False, recurse=False
+ )
+ else:
+ afunc = functools.update_wrapper(
+ functools.partial(run_in_executor, None, func), func
+ )
+ run = RunnableCallable(
+ func,
+ afunc,
+ name=func.__name__,
+ trace=False,
+ recurse=False,
+ )
+ if not _lookup_module_and_qualname(func):
+ return run
+ return CACHE.setdefault(key, run)
+
+
+def get_runnable_for_task(func: Callable[..., Any]) -> Runnable:
+ key = (func, True)
+ if key in CACHE:
+ return CACHE[key]
+ else:
+ if hasattr(func, "__name__"):
+ name = func.__name__
+ elif hasattr(func, "func"):
+ name = func.func.__name__
+ elif hasattr(func, "__class__"):
+ name = func.__class__.__name__
+ else:
+ name = str(func)
+
+ if is_async_callable(func):
+ run = RunnableCallable(
+ None,
+ func,
+ explode_args=True,
+ name=name,
+ trace=False,
+ recurse=False,
+ )
+ else:
+ run = RunnableCallable(
+ func,
+ functools.wraps(func)(functools.partial(run_in_executor, None, func)),
+ explode_args=True,
+ name=name,
+ trace=False,
+ recurse=False,
+ )
+ seq = RunnableSeq(
+ run,
+ ChannelWrite([ChannelWriteEntry(RETURN)]),
+ name=name,
+ trace_inputs=functools.partial(
+ _explode_args_trace_inputs, inspect.signature(func)
+ ),
+ )
+ if not _lookup_module_and_qualname(func):
+ return seq
+ return CACHE.setdefault(key, seq)
+
+
+CACHE: dict[tuple[Callable[..., Any], bool], Runnable] = {}
+
+
+P = ParamSpec("P")
+P1 = TypeVar("P1")
+T = TypeVar("T")
+
+
+class SyncAsyncFuture(Generic[T], concurrent.futures.Future[T]):
+ def __await__(self) -> Generator[T, None, T]:
+ yield cast(T, ...)
+
+
+def call(
+ func: Callable[P, Awaitable[T]] | Callable[P, T],
+ *args: Any,
+ retry_policy: Sequence[RetryPolicy] | None = None,
+ cache_policy: CachePolicy | None = None,
+ **kwargs: Any,
+) -> SyncAsyncFuture[T]:
+ config = get_config()
+ impl = config[CONF][CONFIG_KEY_CALL]
+ fut = impl(
+ func,
+ (args, kwargs),
+ retry_policy=retry_policy,
+ cache_policy=cache_policy,
+ callbacks=config["callbacks"],
+ )
+ return fut
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_checkpoint.py b/langgraph/source/libs/langgraph/langgraph/pregel/_checkpoint.py
new file mode 100644
index 0000000000000000000000000000000000000000..3d510ac7d5ad44670037f89c5d10166d140c2820
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/_checkpoint.py
@@ -0,0 +1,88 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from datetime import datetime, timezone
+
+from langgraph.checkpoint.base import Checkpoint
+from langgraph.checkpoint.base.id import uuid6
+
+from langgraph._internal._typing import MISSING
+from langgraph.channels.base import BaseChannel
+from langgraph.managed.base import ManagedValueMapping, ManagedValueSpec
+
+LATEST_VERSION = 4
+
+
+def empty_checkpoint() -> Checkpoint:
+ return Checkpoint(
+ v=LATEST_VERSION,
+ id=str(uuid6(clock_seq=-2)),
+ ts=datetime.now(timezone.utc).isoformat(),
+ channel_values={},
+ channel_versions={},
+ versions_seen={},
+ )
+
+
+def create_checkpoint(
+ checkpoint: Checkpoint,
+ channels: Mapping[str, BaseChannel] | None,
+ step: int,
+ *,
+ id: str | None = None,
+ updated_channels: set[str] | None = None,
+) -> Checkpoint:
+ """Create a checkpoint for the given channels."""
+ ts = datetime.now(timezone.utc).isoformat()
+ if channels is None:
+ values = checkpoint["channel_values"]
+ else:
+ values = {}
+ for k in channels:
+ if k not in checkpoint["channel_versions"]:
+ continue
+ v = channels[k].checkpoint()
+ if v is not MISSING:
+ values[k] = v
+ return Checkpoint(
+ v=LATEST_VERSION,
+ ts=ts,
+ id=id or str(uuid6(clock_seq=step)),
+ channel_values=values,
+ channel_versions=checkpoint["channel_versions"],
+ versions_seen=checkpoint["versions_seen"],
+ updated_channels=None if updated_channels is None else sorted(updated_channels),
+ )
+
+
+def channels_from_checkpoint(
+ specs: Mapping[str, BaseChannel | ManagedValueSpec],
+ checkpoint: Checkpoint,
+) -> tuple[Mapping[str, BaseChannel], ManagedValueMapping]:
+ """Get channels from a checkpoint."""
+ channel_specs: dict[str, BaseChannel] = {}
+ managed_specs: dict[str, ManagedValueSpec] = {}
+ for k, v in specs.items():
+ if isinstance(v, BaseChannel):
+ channel_specs[k] = v
+ else:
+ managed_specs[k] = v
+ return (
+ {
+ k: v.from_checkpoint(checkpoint["channel_values"].get(k, MISSING))
+ for k, v in channel_specs.items()
+ },
+ managed_specs,
+ )
+
+
+def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint:
+ return Checkpoint(
+ v=checkpoint["v"],
+ ts=checkpoint["ts"],
+ id=checkpoint["id"],
+ channel_values=checkpoint["channel_values"].copy(),
+ channel_versions=checkpoint["channel_versions"].copy(),
+ versions_seen={k: v.copy() for k, v in checkpoint["versions_seen"].items()},
+ updated_channels=checkpoint.get("updated_channels", None),
+ )
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_config.py b/langgraph/source/libs/langgraph/langgraph/pregel/_config.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_draw.py b/langgraph/source/libs/langgraph/langgraph/pregel/_draw.py
new file mode 100644
index 0000000000000000000000000000000000000000..922450df9a8bdaf122f4a4565ca0f7584b3a97fa
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/_draw.py
@@ -0,0 +1,294 @@
+from __future__ import annotations
+
+from collections import defaultdict
+from collections.abc import Mapping, Sequence
+from typing import Any, NamedTuple, cast
+
+from langchain_core.runnables.config import RunnableConfig
+from langchain_core.runnables.graph import Graph, Node
+from langgraph.checkpoint.base import BaseCheckpointSaver
+
+from langgraph._internal._constants import CONF, CONFIG_KEY_SEND, INPUT
+from langgraph.channels.base import BaseChannel
+from langgraph.channels.last_value import LastValueAfterFinish
+from langgraph.constants import END, START
+from langgraph.managed.base import ManagedValueSpec
+from langgraph.pregel._algo import (
+ PregelTaskWrites,
+ apply_writes,
+ increment,
+ prepare_next_tasks,
+)
+from langgraph.pregel._checkpoint import channels_from_checkpoint, empty_checkpoint
+from langgraph.pregel._io import map_input
+from langgraph.pregel._read import PregelNode
+from langgraph.pregel._write import ChannelWrite
+from langgraph.types import All, Checkpointer
+
+
+class Edge(NamedTuple):
+ source: str
+ target: str
+ conditional: bool
+ data: str | None
+
+
+class TriggerEdge(NamedTuple):
+ source: str
+ conditional: bool
+ data: str | None
+
+
+def draw_graph(
+ config: RunnableConfig,
+ *,
+ nodes: dict[str, PregelNode],
+ specs: dict[str, BaseChannel | ManagedValueSpec],
+ input_channels: str | Sequence[str],
+ interrupt_after_nodes: All | Sequence[str],
+ interrupt_before_nodes: All | Sequence[str],
+ trigger_to_nodes: Mapping[str, Sequence[str]],
+ checkpointer: Checkpointer,
+ subgraphs: dict[str, Graph],
+ limit: int = 250,
+) -> Graph:
+ """Get the graph for this Pregel instance.
+
+ Args:
+ config: The configuration to use for the graph.
+ subgraphs: The subgraphs to include in the graph.
+ checkpointer: The checkpointer to use for the graph.
+
+ Returns:
+ The graph for this Pregel instance.
+ """
+ # (src, dest, is_conditional, label)
+ edges: set[Edge] = set()
+
+ step = -1
+ checkpoint = empty_checkpoint()
+ get_next_version = (
+ checkpointer.get_next_version
+ if isinstance(checkpointer, BaseCheckpointSaver)
+ else increment
+ )
+ channels, managed = channels_from_checkpoint(
+ specs,
+ checkpoint,
+ )
+ static_seen: set[Any] = set()
+ sources: dict[str, set[TriggerEdge]] = {}
+ step_sources: dict[str, set[TriggerEdge]] = {}
+ static_declared_writes: dict[str, set[TriggerEdge]] = defaultdict(set)
+ # remove node mappers
+ nodes = {
+ k: v.copy(update={"mapper": None}) if v.mapper is not None else v
+ for k, v in nodes.items()
+ }
+ # apply input writes
+ input_writes = list(map_input(input_channels, {}))
+ updated_channels = apply_writes(
+ checkpoint,
+ channels,
+ [
+ PregelTaskWrites((), INPUT, input_writes, []),
+ ],
+ get_next_version,
+ trigger_to_nodes,
+ )
+ # prepare first tasks
+ tasks = prepare_next_tasks(
+ checkpoint,
+ [],
+ nodes,
+ channels,
+ managed,
+ config,
+ step,
+ -1,
+ for_execution=True,
+ store=None,
+ checkpointer=None,
+ manager=None,
+ trigger_to_nodes=trigger_to_nodes,
+ updated_channels=updated_channels,
+ )
+ start_tasks = tasks
+ # run the pregel loop
+ for step in range(step, limit):
+ if not tasks:
+ break
+ conditionals: dict[tuple[str, str, Any], str | None] = {}
+ # run task writers
+ for task in tasks.values():
+ for w in task.writers:
+ # apply regular writes
+ if isinstance(w, ChannelWrite):
+ empty_input = (
+ cast(BaseChannel, specs["__root__"]).ValueType()
+ if "__root__" in specs
+ else None
+ )
+ w.invoke(empty_input, task.config)
+ # apply conditional writes declared for static analysis, only once
+ if w not in static_seen:
+ static_seen.add(w)
+ # apply static writes
+ if writes := ChannelWrite.get_static_writes(w):
+ # END writes are not written, but become edges directly
+ for t in writes:
+ if t[0] == END:
+ edges.add(Edge(task.name, t[0], True, t[2]))
+ writes = [t for t in writes if t[0] != END]
+ conditionals.update(
+ {(task.name, t[0], t[1] or None): t[2] for t in writes}
+ )
+ # record static writes for edge creation
+ for t in writes:
+ static_declared_writes[task.name].add(
+ TriggerEdge(t[0], True, t[2])
+ )
+ task.config[CONF][CONFIG_KEY_SEND]([t[:2] for t in writes])
+ # collect sources
+ step_sources = {}
+ for task in tasks.values():
+ task_edges = {
+ TriggerEdge(
+ w[0],
+ (task.name, w[0], w[1] or None) in conditionals,
+ conditionals.get((task.name, w[0], w[1] or None)),
+ )
+ for w in task.writes
+ }
+ task_edges |= static_declared_writes.get(task.name, set())
+ step_sources[task.name] = task_edges
+ sources.update(step_sources)
+ # invert triggers
+ trigger_to_sources: dict[str, set[TriggerEdge]] = defaultdict(set)
+ for src, triggers in sources.items():
+ for trigger, cond, label in triggers:
+ trigger_to_sources[trigger].add(TriggerEdge(src, cond, label))
+ # apply writes
+ updated_channels = apply_writes(
+ checkpoint, channels, tasks.values(), get_next_version, trigger_to_nodes
+ )
+ # prepare next tasks
+ tasks = prepare_next_tasks(
+ checkpoint,
+ [],
+ nodes,
+ channels,
+ managed,
+ config,
+ step,
+ limit,
+ for_execution=True,
+ store=None,
+ checkpointer=None,
+ manager=None,
+ trigger_to_nodes=trigger_to_nodes,
+ updated_channels=updated_channels,
+ )
+ # collect deferred nodes
+ deferred_nodes: set[str] = set()
+ edges_to_deferred_nodes: set[Edge] = set()
+ for channel, item in channels.items():
+ if isinstance(item, LastValueAfterFinish):
+ deferred_node = channel.split(":", 2)[-1]
+ deferred_nodes.add(deferred_node)
+ # collect edges
+ for task in tasks.values():
+ added = False
+ for trigger in task.triggers:
+ for src, cond, label in sorted(trigger_to_sources[trigger]):
+ # record edge to be reviewed later
+ if task.name in deferred_nodes:
+ edges_to_deferred_nodes.add(Edge(src, task.name, cond, label))
+ edges.add(Edge(src, task.name, cond, label))
+ # if the edge is from this step, skip adding the implicit edges
+ if (trigger, cond, label) in step_sources.get(src, set()):
+ added = True
+ else:
+ sources[src].discard(TriggerEdge(trigger, cond, label))
+ # if no edges from this step, add implicit edges from all previous tasks
+ if not added:
+ for src in step_sources:
+ edges.add(Edge(src, task.name, True, None))
+
+ # assemble the graph
+ graph = Graph()
+ # add nodes
+ for name, node in nodes.items():
+ metadata = dict(node.metadata or {})
+ if name in deferred_nodes:
+ metadata["defer"] = True
+ if name in interrupt_before_nodes and name in interrupt_after_nodes:
+ metadata["__interrupt"] = "before,after"
+ elif name in interrupt_before_nodes:
+ metadata["__interrupt"] = "before"
+ elif name in interrupt_after_nodes:
+ metadata["__interrupt"] = "after"
+ graph.add_node(node.bound, name, metadata=metadata or None)
+ # add start node
+ if START not in nodes:
+ graph.add_node(None, START)
+ for task in start_tasks.values():
+ add_edge(graph, START, task.name)
+ # add discovered edges
+ for src, dest, is_conditional, label in sorted(edges):
+ add_edge(
+ graph,
+ src,
+ dest,
+ data=label if label != dest else None,
+ conditional=is_conditional,
+ )
+ # add end edges
+ termini = {d for _, d, _, _ in edges if d != END}.difference(
+ s for s, _, _, _ in edges
+ )
+ end_edge_exists = any(d == END for _, d, _, _ in edges)
+ if termini:
+ for src in sorted(termini):
+ add_edge(graph, src, END)
+ elif len(step_sources) == 1 and not end_edge_exists:
+ for src in sorted(step_sources):
+ add_edge(graph, src, END, conditional=True)
+ # replace subgraphs
+ for name, subgraph in subgraphs.items():
+ if (
+ len(subgraph.nodes) > 1
+ and name in graph.nodes
+ and subgraph.first_node()
+ and subgraph.last_node()
+ ):
+ subgraph.trim_first_node()
+ subgraph.trim_last_node()
+ # replace the node with the subgraph
+ graph.nodes.pop(name)
+ first, last = graph.extend(subgraph, prefix=name)
+ for idx, edge in enumerate(graph.edges):
+ if edge.source == name:
+ edge = edge.copy(source=cast(Node, last).id)
+ if edge.target == name:
+ edge = edge.copy(target=cast(Node, first).id)
+ graph.edges[idx] = edge
+
+ return graph
+
+
+def add_edge(
+ graph: Graph,
+ source: str,
+ target: str,
+ *,
+ data: Any | None = None,
+ conditional: bool = False,
+) -> None:
+ """Add an edge to the graph."""
+ for edge in graph.edges:
+ if edge.source == source and edge.target == target:
+ return
+ if target not in graph.nodes and target == END:
+ graph.add_node(None, END)
+ graph.add_edge(graph.nodes[source], graph.nodes[target], data, conditional)
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_executor.py b/langgraph/source/libs/langgraph/langgraph/pregel/_executor.py
new file mode 100644
index 0000000000000000000000000000000000000000..10a43cf2b3cb8e20030bec5f5526b4546089bf8f
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/_executor.py
@@ -0,0 +1,223 @@
+from __future__ import annotations
+
+import asyncio
+import concurrent.futures
+import time
+from collections.abc import Awaitable, Callable, Coroutine
+from contextlib import AbstractAsyncContextManager, AbstractContextManager, ExitStack
+from contextvars import copy_context
+from types import TracebackType
+from typing import (
+ Protocol,
+ TypeVar,
+ cast,
+)
+
+from langchain_core.runnables import RunnableConfig
+from langchain_core.runnables.config import get_executor_for_config
+from typing_extensions import ParamSpec
+
+from langgraph._internal._future import CONTEXT_NOT_SUPPORTED, run_coroutine_threadsafe
+from langgraph.errors import GraphBubbleUp
+
+P = ParamSpec("P")
+T = TypeVar("T")
+
+
+class Submit(Protocol[P, T]):
+ def __call__( # type: ignore[valid-type]
+ self,
+ fn: Callable[P, T],
+ *args: P.args,
+ __name__: str | None = None,
+ __cancel_on_exit__: bool = False,
+ __reraise_on_exit__: bool = True,
+ __next_tick__: bool = False,
+ **kwargs: P.kwargs,
+ ) -> concurrent.futures.Future[T]: ...
+
+
+class BackgroundExecutor(AbstractContextManager):
+ """A context manager that runs sync tasks in the background.
+ Uses a thread pool executor to delegate tasks to separate threads.
+ On exit,
+ - cancels any (not yet started) tasks with `__cancel_on_exit__=True`
+ - waits for all tasks to finish
+ - re-raises the first exception from tasks with `__reraise_on_exit__=True`"""
+
+ def __init__(self, config: RunnableConfig) -> None:
+ self.stack = ExitStack()
+ self.executor = self.stack.enter_context(get_executor_for_config(config))
+ # mapping of Future to (__cancel_on_exit__, __reraise_on_exit__) flags
+ self.tasks: dict[concurrent.futures.Future, tuple[bool, bool]] = {}
+
+ def submit( # type: ignore[valid-type]
+ self,
+ fn: Callable[P, T],
+ *args: P.args,
+ __name__: str | None = None, # currently not used in sync version
+ __cancel_on_exit__: bool = False, # for sync, can cancel only if not started
+ __reraise_on_exit__: bool = True,
+ __next_tick__: bool = False,
+ **kwargs: P.kwargs,
+ ) -> concurrent.futures.Future[T]:
+ ctx = copy_context()
+ if __next_tick__:
+ task = cast(
+ concurrent.futures.Future[T],
+ self.executor.submit(next_tick, ctx.run, fn, *args, **kwargs), # type: ignore[arg-type]
+ )
+ else:
+ task = self.executor.submit(ctx.run, fn, *args, **kwargs)
+ self.tasks[task] = (__cancel_on_exit__, __reraise_on_exit__)
+ # add a callback to remove the task from the tasks dict when it's done
+ task.add_done_callback(self.done)
+ return task
+
+ def done(self, task: concurrent.futures.Future) -> None:
+ """Remove the task from the tasks dict when it's done."""
+ try:
+ task.result()
+ 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)
+ except BaseException:
+ pass
+ else:
+ self.tasks.pop(task)
+
+ def __enter__(self) -> Submit:
+ return self.submit
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> bool | None:
+ # copy the tasks as done() callback may modify the dict
+ tasks = self.tasks.copy()
+ # cancel all tasks that should be cancelled
+ for task, (cancel, _) in tasks.items():
+ if cancel:
+ task.cancel()
+ # wait for all tasks to finish
+ if pending := {t for t in tasks if not t.done()}:
+ concurrent.futures.wait(pending)
+ # shutdown the executor
+ self.stack.__exit__(exc_type, exc_value, traceback)
+ # if there's already an exception being raised, don't raise another one
+ if exc_type is None:
+ # re-raise the first exception that occurred in a task
+ for task, (_, reraise) in tasks.items():
+ if not reraise:
+ continue
+ try:
+ task.result()
+ except concurrent.futures.CancelledError:
+ pass
+
+
+class AsyncBackgroundExecutor(AbstractAsyncContextManager):
+ """A context manager that runs async tasks in the background.
+ Uses the current event loop to delegate tasks to asyncio tasks.
+ On exit,
+ - cancels any tasks with `__cancel_on_exit__=True`
+ - waits for all tasks to finish
+ - re-raises the first exception from tasks with `__reraise_on_exit__=True`
+ ignoring CancelledError"""
+
+ def __init__(self, config: RunnableConfig) -> None:
+ self.tasks: dict[asyncio.Future, tuple[bool, bool]] = {}
+ self.sentinel = object()
+ self.loop = asyncio.get_running_loop()
+ if max_concurrency := config.get("max_concurrency"):
+ self.semaphore: asyncio.Semaphore | None = asyncio.Semaphore(
+ max_concurrency
+ )
+ else:
+ self.semaphore = None
+
+ def submit( # type: ignore[valid-type]
+ self,
+ fn: Callable[P, Awaitable[T]],
+ *args: P.args,
+ __name__: str | None = None,
+ __cancel_on_exit__: bool = False,
+ __reraise_on_exit__: bool = True,
+ __next_tick__: bool = False, # noop in async (always True)
+ **kwargs: P.kwargs,
+ ) -> asyncio.Future[T]:
+ coro = cast(Coroutine[None, None, T], fn(*args, **kwargs))
+ if self.semaphore:
+ coro = gated(self.semaphore, coro)
+ if CONTEXT_NOT_SUPPORTED:
+ task = run_coroutine_threadsafe(
+ coro, self.loop, name=__name__, lazy=__next_tick__
+ )
+ else:
+ task = run_coroutine_threadsafe(
+ coro,
+ self.loop,
+ name=__name__,
+ context=copy_context(),
+ lazy=__next_tick__,
+ )
+ self.tasks[task] = (__cancel_on_exit__, __reraise_on_exit__)
+ task.add_done_callback(self.done)
+ return task
+
+ def done(self, task: asyncio.Future) -> None:
+ try:
+ 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, GraphBubbleUp):
+ self.tasks.pop(task)
+ else:
+ self.tasks.pop(task)
+ except asyncio.CancelledError:
+ self.tasks.pop(task)
+
+ async def __aenter__(self) -> Submit:
+ return self.submit
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> None:
+ # copy the tasks as done() callback may modify the dict
+ tasks = self.tasks.copy()
+ # cancel all tasks that should be cancelled
+ for task, (cancel, _) in tasks.items():
+ if cancel:
+ task.cancel(self.sentinel)
+ # wait for all tasks to finish
+ if tasks:
+ await asyncio.wait(tasks)
+ # if there's already an exception being raised, don't raise another one
+ if exc_type is None:
+ # re-raise the first exception that occurred in a task
+ for task, (_, reraise) in tasks.items():
+ if not reraise:
+ continue
+ try:
+ if exc := task.exception():
+ raise exc
+ except asyncio.CancelledError:
+ pass
+
+
+async def gated(semaphore: asyncio.Semaphore, coro: Coroutine[None, None, T]) -> T:
+ """A coroutine that waits for a semaphore before running another coroutine."""
+ async with semaphore:
+ return await coro
+
+
+def next_tick(fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
+ """A function that yields control to other threads before running another function."""
+ time.sleep(0)
+ return fn(*args, **kwargs)
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_io.py b/langgraph/source/libs/langgraph/langgraph/pregel/_io.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c05dbda70ed04f1038268a201000d5141535832
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/_io.py
@@ -0,0 +1,174 @@
+from __future__ import annotations
+
+from collections import Counter
+from collections.abc import Iterator, Mapping, Sequence
+from typing import Any, Literal
+
+from langgraph._internal._constants import (
+ ERROR,
+ INTERRUPT,
+ NULL_TASK_ID,
+ RESUME,
+ RETURN,
+ TASKS,
+)
+from langgraph._internal._typing import EMPTY_SEQ, MISSING
+from langgraph.channels.base import BaseChannel, EmptyChannelError
+from langgraph.constants import START, TAG_HIDDEN
+from langgraph.errors import InvalidUpdateError
+from langgraph.pregel._log import logger
+from langgraph.types import Command, PregelExecutableTask, Send
+
+
+def read_channel(
+ channels: Mapping[str, BaseChannel],
+ chan: str,
+ *,
+ catch: bool = True,
+) -> Any:
+ try:
+ return channels[chan].get()
+ except EmptyChannelError:
+ if catch:
+ return None
+ else:
+ raise
+
+
+def read_channels(
+ channels: Mapping[str, BaseChannel],
+ select: Sequence[str] | str,
+ *,
+ skip_empty: bool = True,
+) -> dict[str, Any] | Any:
+ if isinstance(select, str):
+ return read_channel(channels, select)
+ else:
+ values: dict[str, Any] = {}
+ for k in select:
+ try:
+ values[k] = read_channel(channels, k, catch=not skip_empty)
+ except EmptyChannelError:
+ pass
+ return values
+
+
+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 no parent graph")
+ if cmd.goto:
+ if isinstance(cmd.goto, (tuple, list)):
+ sends = cmd.goto
+ else:
+ sends = [cmd.goto]
+ for send in sends:
+ if isinstance(send, Send):
+ yield (NULL_TASK_ID, TASKS, send)
+ elif isinstance(send, str):
+ yield (NULL_TASK_ID, f"branch:to:{send}", START)
+ else:
+ raise TypeError(
+ f"In Command.goto, expected Send/str, got {type(send).__name__}"
+ )
+ if cmd.resume is not None:
+ yield (NULL_TASK_ID, RESUME, cmd.resume)
+ if cmd.update:
+ for k, v in cmd._update_as_tuples():
+ yield (NULL_TASK_ID, k, v)
+
+
+def map_input(
+ input_channels: str | Sequence[str],
+ chunk: dict[str, Any] | Any | None,
+) -> Iterator[tuple[str, Any]]:
+ """Map input chunk to a sequence of pending writes in the form (channel, value)."""
+ if chunk is None:
+ return
+ elif isinstance(input_channels, str):
+ yield (input_channels, chunk)
+ else:
+ if not isinstance(chunk, dict):
+ raise TypeError(f"Expected chunk to be a dict, got {type(chunk).__name__}")
+ for k in chunk:
+ if k in input_channels:
+ yield (k, chunk[k])
+ else:
+ logger.warning(f"Input channel {k} not found in {input_channels}")
+
+
+def map_output_values(
+ output_channels: str | Sequence[str],
+ pending_writes: Literal[True] | Sequence[tuple[str, Any]],
+ channels: Mapping[str, BaseChannel],
+) -> Iterator[dict[str, Any] | Any]:
+ """Map pending writes (a sequence of tuples (channel, value)) to output chunk."""
+ if isinstance(output_channels, str):
+ if pending_writes is True or any(
+ chan == output_channels for chan, _ in pending_writes
+ ):
+ yield read_channel(channels, output_channels)
+ else:
+ if pending_writes is True or {
+ c for c, _ in pending_writes if c in output_channels
+ }:
+ yield read_channels(channels, output_channels)
+
+
+def map_output_updates(
+ output_channels: str | Sequence[str],
+ tasks: list[tuple[PregelExecutableTask, Sequence[tuple[str, Any]]]],
+ cached: bool = False,
+) -> Iterator[dict[str, Any | dict[str, Any]]]:
+ """Map pending writes (a sequence of tuples (channel, value)) to output chunk."""
+ output_tasks = [
+ (t, ww)
+ for t, ww in tasks
+ if (not t.config or TAG_HIDDEN not in t.config.get("tags", EMPTY_SEQ))
+ and ww[0][0] != ERROR
+ and ww[0][0] != INTERRUPT
+ ]
+ if not output_tasks:
+ return
+ updated: list[tuple[str, Any]] = []
+ for task, writes in output_tasks:
+ rtn = next((value for chan, value in writes if chan == RETURN), MISSING)
+ if rtn is not MISSING:
+ updated.append((task.name, rtn))
+ elif isinstance(output_channels, str):
+ updated.extend(
+ (task.name, value) for chan, value in writes if chan == output_channels
+ )
+ elif any(chan in output_channels for chan, _ in writes):
+ counts = Counter(chan for chan, _ in writes)
+ if any(counts[chan] > 1 for chan in output_channels):
+ updated.extend(
+ (
+ task.name,
+ {chan: value},
+ )
+ for chan, value in writes
+ if chan in output_channels
+ )
+ else:
+ updated.append(
+ (
+ task.name,
+ {
+ chan: value
+ for chan, value in writes
+ if chan in output_channels
+ },
+ )
+ )
+ grouped: dict[str, Any] = {t.name: [] for t, _ in output_tasks}
+ for node, value in updated:
+ grouped[node].append(value)
+ for node, value in grouped.items():
+ if len(value) == 0:
+ grouped[node] = None
+ if len(value) == 1:
+ grouped[node] = value[0]
+ if cached:
+ grouped["__metadata__"] = {"cached": cached}
+ yield grouped
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_log.py b/langgraph/source/libs/langgraph/langgraph/pregel/_log.py
new file mode 100644
index 0000000000000000000000000000000000000000..fd127777b23fab1a3daa78b552a93b1a13ce38b1
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/_log.py
@@ -0,0 +1,3 @@
+import logging
+
+logger = logging.getLogger("langgraph")
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_loop.py b/langgraph/source/libs/langgraph/langgraph/pregel/_loop.py
new file mode 100644
index 0000000000000000000000000000000000000000..79201d0d6e173c36d02b8743ce6672119f2b2649
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/_loop.py
@@ -0,0 +1,1328 @@
+from __future__ import annotations
+
+import asyncio
+import binascii
+import concurrent.futures
+from collections import defaultdict, deque
+from collections.abc import Callable, Iterator, Mapping, Sequence
+from contextlib import (
+ AbstractAsyncContextManager,
+ AbstractContextManager,
+ AsyncExitStack,
+ ExitStack,
+)
+from datetime import datetime, timezone
+from inspect import signature
+from types import TracebackType
+from typing import (
+ Any,
+ Literal,
+ TypeVar,
+ cast,
+)
+
+from langchain_core.callbacks import AsyncParentRunManager, ParentRunManager
+from langchain_core.runnables import RunnableConfig
+from langgraph.cache.base import BaseCache
+from langgraph.checkpoint.base import (
+ WRITES_IDX_MAP,
+ BaseCheckpointSaver,
+ ChannelVersions,
+ Checkpoint,
+ CheckpointMetadata,
+ CheckpointTuple,
+ PendingWrite,
+)
+from langgraph.store.base import BaseStore
+from typing_extensions import ParamSpec, Self
+
+from langgraph._internal._config import patch_configurable
+from langgraph._internal._constants import (
+ CONF,
+ CONFIG_KEY_CHECKPOINT_ID,
+ CONFIG_KEY_CHECKPOINT_MAP,
+ CONFIG_KEY_CHECKPOINT_NS,
+ CONFIG_KEY_RESUME_MAP,
+ CONFIG_KEY_RESUMING,
+ CONFIG_KEY_SCRATCHPAD,
+ CONFIG_KEY_STREAM,
+ CONFIG_KEY_TASK_ID,
+ CONFIG_KEY_THREAD_ID,
+ ERROR,
+ INPUT,
+ INTERRUPT,
+ NS_END,
+ NS_SEP,
+ NULL_TASK_ID,
+ PUSH,
+ RESUME,
+ TASKS,
+)
+from langgraph._internal._scratchpad import PregelScratchpad
+from langgraph._internal._typing import EMPTY_SEQ, MISSING
+from langgraph.channels.base import BaseChannel
+from langgraph.channels.untracked_value import UntrackedValue
+from langgraph.constants import TAG_HIDDEN
+from langgraph.errors import (
+ EmptyInputError,
+ GraphInterrupt,
+)
+from langgraph.managed.base import (
+ ManagedValueMapping,
+ ManagedValueSpec,
+)
+from langgraph.pregel._algo import (
+ Call,
+ GetNextVersion,
+ PregelTaskWrites,
+ apply_writes,
+ checkpoint_null_version,
+ increment,
+ prepare_next_tasks,
+ prepare_single_task,
+ sanitize_untracked_values_in_send,
+ should_interrupt,
+ task_path_str,
+)
+from langgraph.pregel._checkpoint import (
+ channels_from_checkpoint,
+ copy_checkpoint,
+ create_checkpoint,
+ empty_checkpoint,
+)
+from langgraph.pregel._executor import (
+ AsyncBackgroundExecutor,
+ BackgroundExecutor,
+ Submit,
+)
+from langgraph.pregel._io import (
+ map_command,
+ map_input,
+ map_output_updates,
+ map_output_values,
+ read_channels,
+)
+from langgraph.pregel._read import PregelNode
+from langgraph.pregel._utils import get_new_channel_versions, is_xxh3_128_hexdigest
+from langgraph.pregel.debug import (
+ map_debug_checkpoint,
+ map_debug_task_results,
+ map_debug_tasks,
+)
+from langgraph.pregel.protocol import StreamChunk, StreamProtocol
+from langgraph.types import (
+ All,
+ CachePolicy,
+ Command,
+ Durability,
+ PregelExecutableTask,
+ RetryPolicy,
+ Send,
+ StreamMode,
+)
+
+V = TypeVar("V")
+P = ParamSpec("P")
+
+
+WritesT = Sequence[tuple[str, Any]]
+
+
+def DuplexStream(*streams: StreamProtocol) -> StreamProtocol:
+ def __call__(value: StreamChunk) -> None:
+ for stream in streams:
+ if value[1] in stream.modes:
+ stream(value)
+
+ return StreamProtocol(__call__, {mode for s in streams for mode in s.modes})
+
+
+class PregelLoop:
+ config: RunnableConfig
+ store: BaseStore | None
+ stream: StreamProtocol | None
+ step: int
+ stop: int
+
+ input: Any | None
+ cache: BaseCache[WritesT] | None
+ checkpointer: BaseCheckpointSaver | None
+ nodes: Mapping[str, PregelNode]
+ specs: Mapping[str, BaseChannel | ManagedValueSpec]
+ input_keys: str | Sequence[str]
+ output_keys: str | Sequence[str]
+ stream_keys: str | Sequence[str]
+ skip_done_tasks: bool
+ is_nested: bool
+ manager: None | AsyncParentRunManager | ParentRunManager
+ interrupt_after: All | Sequence[str]
+ interrupt_before: All | Sequence[str]
+ durability: Durability
+ retry_policy: Sequence[RetryPolicy]
+ cache_policy: CachePolicy | None
+
+ checkpointer_get_next_version: GetNextVersion
+ checkpointer_put_writes: Callable[[RunnableConfig, WritesT, str], Any] | None
+ checkpointer_put_writes_accepts_task_path: bool
+ _checkpointer_put_after_previous: (
+ Callable[
+ [
+ concurrent.futures.Future | None,
+ RunnableConfig,
+ Checkpoint,
+ str,
+ ChannelVersions,
+ ],
+ Any,
+ ]
+ | None
+ )
+ _migrate_checkpoint: Callable[[Checkpoint], None] | None
+ submit: Submit
+ channels: Mapping[str, BaseChannel]
+ managed: ManagedValueMapping
+ checkpoint: Checkpoint
+ checkpoint_id_saved: str
+ checkpoint_ns: tuple[str, ...]
+ checkpoint_config: RunnableConfig
+ checkpoint_metadata: CheckpointMetadata
+ checkpoint_pending_writes: list[PendingWrite]
+ checkpoint_previous_versions: dict[str, str | float | int]
+ prev_checkpoint_config: RunnableConfig | None
+
+ status: Literal[
+ "input",
+ "pending",
+ "done",
+ "interrupt_before",
+ "interrupt_after",
+ "out_of_steps",
+ ]
+ tasks: dict[str, PregelExecutableTask]
+ output: None | dict[str, Any] | Any = None
+ updated_channels: set[str] | None = None
+
+ # public
+
+ def __init__(
+ self,
+ input: Any | None,
+ *,
+ stream: StreamProtocol | None,
+ config: RunnableConfig,
+ store: BaseStore | None,
+ cache: BaseCache | None,
+ checkpointer: BaseCheckpointSaver | None,
+ nodes: Mapping[str, PregelNode],
+ specs: Mapping[str, BaseChannel | ManagedValueSpec],
+ input_keys: str | Sequence[str],
+ output_keys: str | Sequence[str],
+ stream_keys: str | Sequence[str],
+ trigger_to_nodes: Mapping[str, Sequence[str]],
+ durability: Durability,
+ interrupt_after: All | Sequence[str] = EMPTY_SEQ,
+ interrupt_before: All | Sequence[str] = EMPTY_SEQ,
+ manager: None | AsyncParentRunManager | ParentRunManager = None,
+ migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
+ retry_policy: Sequence[RetryPolicy] = (),
+ cache_policy: CachePolicy | None = None,
+ ) -> None:
+ self.stream = stream
+ self.config = config
+ self.store = store
+ self.step = 0
+ self.stop = 0
+ self.input = input
+ self.checkpointer = checkpointer
+ self.cache = cache
+ self.nodes = nodes
+ self.specs = specs
+ self.input_keys = input_keys
+ self.output_keys = output_keys
+ self.stream_keys = stream_keys
+ self.interrupt_after = interrupt_after
+ self.interrupt_before = interrupt_before
+ self.manager = manager
+ self.is_nested = CONFIG_KEY_TASK_ID in self.config.get(CONF, {})
+ self.skip_done_tasks = CONFIG_KEY_CHECKPOINT_ID not in config[CONF]
+ self._migrate_checkpoint = migrate_checkpoint
+ self.trigger_to_nodes = trigger_to_nodes
+ self.retry_policy = retry_policy
+ self.cache_policy = cache_policy
+ self.durability = durability
+ if self.stream is not None and CONFIG_KEY_STREAM in config[CONF]:
+ self.stream = DuplexStream(self.stream, config[CONF][CONFIG_KEY_STREAM])
+ scratchpad: PregelScratchpad | None = config[CONF].get(CONFIG_KEY_SCRATCHPAD)
+ if isinstance(scratchpad, PregelScratchpad):
+ # if count is > 0, append to checkpoint_ns
+ # if count is 0, leave as is
+ if cnt := scratchpad.subgraph_counter():
+ self.config = patch_configurable(
+ self.config,
+ {
+ CONFIG_KEY_CHECKPOINT_NS: NS_SEP.join(
+ (
+ config[CONF][CONFIG_KEY_CHECKPOINT_NS],
+ str(cnt),
+ )
+ )
+ },
+ )
+ if not self.is_nested and config[CONF].get(CONFIG_KEY_CHECKPOINT_NS):
+ self.config = patch_configurable(
+ self.config,
+ {CONFIG_KEY_CHECKPOINT_NS: "", CONFIG_KEY_CHECKPOINT_ID: None},
+ )
+ if (
+ CONFIG_KEY_CHECKPOINT_MAP in self.config[CONF]
+ and self.config[CONF].get(CONFIG_KEY_CHECKPOINT_NS)
+ in self.config[CONF][CONFIG_KEY_CHECKPOINT_MAP]
+ ):
+ self.checkpoint_config = patch_configurable(
+ self.config,
+ {
+ CONFIG_KEY_CHECKPOINT_ID: self.config[CONF][
+ CONFIG_KEY_CHECKPOINT_MAP
+ ][self.config[CONF][CONFIG_KEY_CHECKPOINT_NS]]
+ },
+ )
+ else:
+ self.checkpoint_config = self.config
+ if thread_id := self.checkpoint_config[CONF].get(CONFIG_KEY_THREAD_ID):
+ if not isinstance(thread_id, str):
+ self.checkpoint_config = patch_configurable(
+ self.checkpoint_config,
+ {CONFIG_KEY_THREAD_ID: str(thread_id)},
+ )
+ self.checkpoint_ns = (
+ tuple(cast(str, self.config[CONF][CONFIG_KEY_CHECKPOINT_NS]).split(NS_SEP))
+ if self.config[CONF].get(CONFIG_KEY_CHECKPOINT_NS)
+ else ()
+ )
+ self.prev_checkpoint_config = None
+
+ def put_writes(self, task_id: str, writes: WritesT) -> None:
+ """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())
+ if task_id == NULL_TASK_ID:
+ # writes for the null task are accumulated
+ self.checkpoint_pending_writes = [
+ w
+ for w in self.checkpoint_pending_writes
+ if w[0] != task_id or w[1] not in WRITES_IDX_MAP
+ ]
+ writes_to_save: WritesT = [
+ w[1:] for w in self.checkpoint_pending_writes if w[0] == task_id
+ ] + list(writes)
+ else:
+ # remove existing writes for this task
+ self.checkpoint_pending_writes = [
+ w for w in self.checkpoint_pending_writes if w[0] != task_id
+ ]
+ writes_to_save = writes
+
+ # check if any writes are to an UntrackedValue channel
+ if any(
+ isinstance(channel, UntrackedValue) for channel in self.channels.values()
+ ):
+ # we do not persist untracked values in checkpoints
+ writes_to_save = [
+ # sanitize UntrackedValues that are nested within Send packets
+ (
+ (c, sanitize_untracked_values_in_send(v, self.channels))
+ if c == TASKS and isinstance(v, Send)
+ else (c, v)
+ )
+ for c, v in writes_to_save
+ # dont persist UntrackedValue channel writes
+ if not isinstance(self.specs.get(c), UntrackedValue)
+ ]
+
+ # save writes
+ self.checkpoint_pending_writes.extend((task_id, c, v) for c, v in writes)
+ if self.durability != "exit" and self.checkpointer_put_writes is not None:
+ config = patch_configurable(
+ self.checkpoint_config,
+ {
+ CONFIG_KEY_CHECKPOINT_NS: self.config[CONF].get(
+ CONFIG_KEY_CHECKPOINT_NS, ""
+ ),
+ CONFIG_KEY_CHECKPOINT_ID: self.checkpoint["id"],
+ },
+ )
+ if self.checkpointer_put_writes_accepts_task_path:
+ if hasattr(self, "tasks"):
+ task = self.tasks.get(task_id)
+ else:
+ task = None
+ self.submit(
+ self.checkpointer_put_writes,
+ config,
+ writes_to_save,
+ task_id,
+ task_path_str(task.path) if task else "",
+ )
+ else:
+ self.submit(
+ self.checkpointer_put_writes,
+ config,
+ writes_to_save,
+ task_id,
+ )
+ # output writes
+ if hasattr(self, "tasks"):
+ self.output_writes(task_id, writes)
+
+ def _put_pending_writes(self) -> None:
+ if self.checkpointer_put_writes is None:
+ return
+ if not self.checkpoint_pending_writes:
+ return
+ # patch config
+ config = patch_configurable(
+ self.checkpoint_config,
+ {
+ CONFIG_KEY_CHECKPOINT_NS: self.config[CONF].get(
+ CONFIG_KEY_CHECKPOINT_NS, ""
+ ),
+ CONFIG_KEY_CHECKPOINT_ID: self.checkpoint["id"],
+ },
+ )
+ # group by task id
+ by_task = defaultdict(list)
+ for task_id, channel, value in self.checkpoint_pending_writes:
+ by_task[task_id].append((channel, value))
+ # submit writes to checkpointer
+ for task_id, writes in by_task.items():
+ if self.checkpointer_put_writes_accepts_task_path and hasattr(
+ self, "tasks"
+ ):
+ task = self.tasks.get(task_id)
+ self.submit(
+ self.checkpointer_put_writes,
+ config,
+ writes,
+ task_id,
+ task_path_str(task.path) if task else "",
+ )
+ else:
+ self.submit(
+ self.checkpointer_put_writes,
+ config,
+ writes,
+ task_id,
+ )
+
+ def accept_push(
+ self, task: PregelExecutableTask, write_idx: int, call: Call | None = None
+ ) -> PregelExecutableTask | None:
+ """Accept a PUSH from a task, potentially returning a new task to start."""
+ checkpoint_id_bytes = binascii.unhexlify(self.checkpoint["id"].replace("-", ""))
+ null_version = checkpoint_null_version(self.checkpoint)
+ if pushed := cast(
+ PregelExecutableTask | None,
+ prepare_single_task(
+ (PUSH, task.path, write_idx, task.id, call),
+ None,
+ checkpoint=self.checkpoint,
+ checkpoint_id_bytes=checkpoint_id_bytes,
+ checkpoint_null_version=null_version,
+ pending_writes=self.checkpoint_pending_writes,
+ processes=self.nodes,
+ channels=self.channels,
+ managed=self.managed,
+ config=task.config,
+ step=self.step,
+ stop=self.stop,
+ for_execution=True,
+ store=self.store,
+ checkpointer=self.checkpointer,
+ manager=self.manager,
+ retry_policy=self.retry_policy,
+ cache_policy=self.cache_policy,
+ ),
+ ):
+ # produce debug output
+ self._emit("tasks", map_debug_tasks, [pushed])
+ # save the new task
+ self.tasks[pushed.id] = pushed
+ # match any pending writes to the new task
+ if self.skip_done_tasks:
+ self._match_writes({pushed.id: pushed})
+ # return the new task, to be started if not run before
+ return pushed
+
+ def tick(self) -> bool:
+ """Execute a single iteration of the Pregel loop.
+
+ Returns:
+ True if more iterations are needed.
+ """
+
+ # check if iteration limit is reached
+ if self.step > self.stop:
+ self.status = "out_of_steps"
+ return False
+
+ # prepare next tasks
+ self.tasks = prepare_next_tasks(
+ self.checkpoint,
+ self.checkpoint_pending_writes,
+ self.nodes,
+ self.channels,
+ self.managed,
+ self.config,
+ self.step,
+ self.stop,
+ for_execution=True,
+ manager=self.manager,
+ store=self.store,
+ checkpointer=self.checkpointer,
+ trigger_to_nodes=self.trigger_to_nodes,
+ updated_channels=self.updated_channels,
+ retry_policy=self.retry_policy,
+ cache_policy=self.cache_policy,
+ )
+
+ # produce debug output
+ if self._checkpointer_put_after_previous is not None:
+ self._emit(
+ "checkpoints",
+ map_debug_checkpoint,
+ {
+ **self.checkpoint_config,
+ CONF: {
+ **self.checkpoint_config[CONF],
+ CONFIG_KEY_CHECKPOINT_ID: self.checkpoint["id"],
+ },
+ },
+ self.channels,
+ self.stream_keys,
+ self.checkpoint_metadata,
+ self.tasks.values(),
+ self.checkpoint_pending_writes,
+ self.prev_checkpoint_config,
+ self.output_keys,
+ )
+
+ # if no more tasks, we're done
+ if not self.tasks:
+ self.status = "done"
+ return False
+
+ # if there are pending writes from a previous loop, apply them
+ if self.skip_done_tasks and self.checkpoint_pending_writes:
+ self._match_writes(self.tasks)
+
+ # before execution, check if we should interrupt
+ if self.interrupt_before and should_interrupt(
+ self.checkpoint, self.interrupt_before, self.tasks.values()
+ ):
+ self.status = "interrupt_before"
+ raise GraphInterrupt()
+
+ # produce debug output
+ self._emit("tasks", map_debug_tasks, self.tasks.values())
+
+ # print output for any tasks we applied previous writes to
+ for task in self.tasks.values():
+ if task.writes:
+ self.output_writes(task.id, task.writes, cached=True)
+
+ return True
+
+ def after_tick(self) -> None:
+ # finish superstep
+ writes = [w for t in self.tasks.values() for w in t.writes]
+ # all tasks have finished
+ self.updated_channels = apply_writes(
+ self.checkpoint,
+ self.channels,
+ self.tasks.values(),
+ self.checkpointer_get_next_version,
+ self.trigger_to_nodes,
+ )
+ # produce values output
+ if not self.updated_channels.isdisjoint(
+ (self.output_keys,)
+ if isinstance(self.output_keys, str)
+ else self.output_keys
+ ):
+ self._emit(
+ "values", map_output_values, self.output_keys, writes, self.channels
+ )
+ # clear pending writes
+ self.checkpoint_pending_writes.clear()
+ # "not skip_done_tasks" only applies to first tick after resuming
+ self.skip_done_tasks = True
+ # save checkpoint
+ self._put_checkpoint({"source": "loop"})
+ # after execution, check if we should interrupt
+ if self.interrupt_after and should_interrupt(
+ self.checkpoint, self.interrupt_after, self.tasks.values()
+ ):
+ self.status = "interrupt_after"
+ raise GraphInterrupt()
+ # unset resuming flag
+ self.config[CONF].pop(CONFIG_KEY_RESUMING, None)
+
+ def match_cached_writes(self) -> Sequence[PregelExecutableTask]:
+ raise NotImplementedError
+
+ async def amatch_cached_writes(self) -> Sequence[PregelExecutableTask]:
+ raise NotImplementedError
+
+ # private
+
+ def _match_writes(self, tasks: Mapping[str, PregelExecutableTask]) -> None:
+ for tid, k, v in self.checkpoint_pending_writes:
+ if k in (ERROR, INTERRUPT, RESUME):
+ continue
+ if task := tasks.get(tid):
+ task.writes.append((k, v))
+
+ def _pending_interrupts(self) -> set[str]:
+ """Return the set of interrupt ids that are pending without corresponding resume values."""
+ # mapping of task ids to interrupt ids
+ pending_interrupts: dict[str, str] = {}
+
+ # set of resume task ids
+ pending_resumes: set[str] = set()
+
+ for task_id, write_type, value in self.checkpoint_pending_writes:
+ if write_type == INTERRUPT:
+ # interrupts is always a list, but there should only be one element
+ pending_interrupts[task_id] = value[0].id
+ elif write_type == RESUME:
+ pending_resumes.add(task_id)
+
+ resumed_interrupt_ids = {
+ pending_interrupts[task_id]
+ for task_id in pending_resumes
+ if task_id in pending_interrupts
+ }
+
+ # Keep only interrupts whose interrupt_id is not resumed
+ hanging_interrupts: set[str] = {
+ interrupt_id
+ for interrupt_id in pending_interrupts.values()
+ if interrupt_id not in resumed_interrupt_ids
+ }
+
+ return hanging_interrupts
+
+ def _first(
+ self, *, input_keys: str | Sequence[str], updated_channels: set[str] | None
+ ) -> set[str] | None:
+ # resuming from previous checkpoint requires
+ # - finding a previous checkpoint
+ # - receiving None input (outer graph) or RESUMING flag (subgraph)
+ configurable = self.config.get(CONF, {})
+ is_resuming = bool(self.checkpoint["channel_versions"]) and bool(
+ configurable.get(
+ CONFIG_KEY_RESUMING,
+ self.input is None
+ or isinstance(self.input, Command)
+ or (
+ not self.is_nested
+ and self.config.get("metadata", {}).get("run_id")
+ == self.checkpoint_metadata.get("run_id", MISSING)
+ ),
+ )
+ )
+
+ # map command to writes
+ if isinstance(self.input, Command):
+ if (resume := self.input.resume) is not None:
+ if not self.checkpointer:
+ raise RuntimeError(
+ "Cannot use Command(resume=...) without checkpointer"
+ )
+
+ if resume_is_map := (
+ isinstance(resume, dict)
+ and all(is_xxh3_128_hexdigest(k) for k in resume)
+ ):
+ self.config[CONF][CONFIG_KEY_RESUME_MAP] = resume
+ else:
+ if len(self._pending_interrupts()) > 1:
+ raise RuntimeError(
+ "When there are multiple pending interrupts, you must specify the interrupt id when resuming. "
+ "Docs: https://docs.langchain.com/oss/python/langgraph/add-human-in-the-loop#resume-multiple-interrupts-with-one-invocation."
+ )
+
+ writes: defaultdict[str, list[tuple[str, Any]]] = defaultdict(list)
+ # group writes by task ID
+ for tid, c, v in map_command(cmd=self.input):
+ if not (c == RESUME and resume_is_map):
+ writes[tid].append((c, v))
+ if not writes and not resume_is_map:
+ raise EmptyInputError("Received empty Command input")
+ # save writes
+ for tid, ws in writes.items():
+ self.put_writes(tid, ws)
+ # apply NULL writes
+ if null_writes := [
+ w[1:] for w in self.checkpoint_pending_writes if w[0] == NULL_TASK_ID
+ ]:
+ null_updated_channels = apply_writes(
+ self.checkpoint,
+ self.channels,
+ [PregelTaskWrites((), INPUT, null_writes, [])],
+ self.checkpointer_get_next_version,
+ self.trigger_to_nodes,
+ )
+ if updated_channels is not None:
+ updated_channels.update(null_updated_channels)
+ # proceed past previous checkpoint
+ if is_resuming:
+ self.checkpoint["versions_seen"].setdefault(INTERRUPT, {})
+ for k in self.channels:
+ if k in self.checkpoint["channel_versions"]:
+ version = self.checkpoint["channel_versions"][k]
+ self.checkpoint["versions_seen"][INTERRUPT][k] = version
+ # produce values output
+ self._emit(
+ "values", map_output_values, self.output_keys, True, self.channels
+ )
+ # map inputs to channel updates
+ elif input_writes := deque(map_input(input_keys, self.input)):
+ # discard any unfinished tasks from previous checkpoint
+ discard_tasks = prepare_next_tasks(
+ self.checkpoint,
+ self.checkpoint_pending_writes,
+ self.nodes,
+ self.channels,
+ self.managed,
+ self.config,
+ self.step,
+ self.stop,
+ for_execution=True,
+ store=None,
+ checkpointer=None,
+ manager=None,
+ updated_channels=updated_channels,
+ )
+ # apply input writes
+ updated_channels = apply_writes(
+ self.checkpoint,
+ self.channels,
+ [
+ *discard_tasks.values(),
+ PregelTaskWrites((), INPUT, input_writes, []),
+ ],
+ self.checkpointer_get_next_version,
+ self.trigger_to_nodes,
+ )
+ # save input checkpoint
+ self.updated_channels = updated_channels
+ self._put_checkpoint({"source": "input"})
+ elif CONFIG_KEY_RESUMING not in configurable:
+ raise EmptyInputError(f"Received no input for {input_keys}")
+ # update config
+ if not self.is_nested:
+ self.config = patch_configurable(
+ self.config, {CONFIG_KEY_RESUMING: is_resuming}
+ )
+ # set flag
+ self.status = "pending"
+ return updated_channels
+
+ def _put_checkpoint(self, metadata: CheckpointMetadata) -> None:
+ # assign step and parents
+ exiting = metadata is self.checkpoint_metadata
+ if exiting and self.checkpoint["id"] == self.checkpoint_id_saved:
+ # checkpoint already saved
+ return
+ if not exiting:
+ metadata["step"] = self.step
+ metadata["parents"] = self.config[CONF].get(CONFIG_KEY_CHECKPOINT_MAP, {})
+ self.checkpoint_metadata = metadata
+ # do checkpoint?
+ do_checkpoint = self._checkpointer_put_after_previous is not None and (
+ exiting or self.durability != "exit"
+ )
+ # create new checkpoint
+ self.checkpoint = create_checkpoint(
+ self.checkpoint,
+ self.channels if do_checkpoint else None,
+ self.step,
+ id=self.checkpoint["id"] if exiting else None,
+ updated_channels=self.updated_channels,
+ )
+ # sanitize TASK channel in the checkpoint before saving (durability=="exit")
+ if TASKS in self.checkpoint["channel_values"] and any(
+ isinstance(channel, UntrackedValue) for channel in self.channels.values()
+ ):
+ sanitized_tasks = [
+ sanitize_untracked_values_in_send(value, self.channels)
+ if isinstance(value, Send)
+ else value
+ for value in self.checkpoint["channel_values"][TASKS]
+ ]
+ self.checkpoint["channel_values"][TASKS] = sanitized_tasks
+ # bail if no checkpointer
+
+ if do_checkpoint and self._checkpointer_put_after_previous is not None:
+ self.prev_checkpoint_config = (
+ self.checkpoint_config
+ if CONFIG_KEY_CHECKPOINT_ID in self.checkpoint_config[CONF]
+ and self.checkpoint_config[CONF][CONFIG_KEY_CHECKPOINT_ID]
+ else None
+ )
+ self.checkpoint_config = {
+ **self.checkpoint_config,
+ CONF: {
+ **self.checkpoint_config[CONF],
+ CONFIG_KEY_CHECKPOINT_NS: self.config[CONF].get(
+ CONFIG_KEY_CHECKPOINT_NS, ""
+ ),
+ },
+ }
+
+ channel_versions = self.checkpoint["channel_versions"].copy()
+ new_versions = get_new_channel_versions(
+ self.checkpoint_previous_versions, channel_versions
+ )
+ self.checkpoint_previous_versions = channel_versions
+
+ # save it, without blocking
+ # if there's a previous checkpoint save in progress, wait for it
+ # ensuring checkpointers receive checkpoints in order
+ self._put_checkpoint_fut = self.submit(
+ self._checkpointer_put_after_previous,
+ getattr(self, "_put_checkpoint_fut", None),
+ self.checkpoint_config,
+ copy_checkpoint(self.checkpoint),
+ self.checkpoint_metadata,
+ new_versions,
+ )
+ self.checkpoint_config = {
+ **self.checkpoint_config,
+ CONF: {
+ **self.checkpoint_config[CONF],
+ CONFIG_KEY_CHECKPOINT_ID: self.checkpoint["id"],
+ },
+ }
+ if not exiting:
+ # increment step
+ self.step += 1
+
+ def _suppress_interrupt(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> bool | None:
+ # persist current checkpoint and writes
+ if self.durability == "exit" and (
+ # if it's a top graph
+ not self.is_nested
+ # or a nested graph with error or interrupt
+ or exc_value is not None
+ # or a nested graph with checkpointer=True
+ or all(NS_END not in part for part in self.checkpoint_ns)
+ ):
+ self._put_checkpoint(self.checkpoint_metadata)
+ self._put_pending_writes()
+ # suppress interrupt
+ suppress = isinstance(exc_value, GraphInterrupt) and not self.is_nested
+ if suppress:
+ # emit one last "values" event, with pending writes applied
+ if (
+ hasattr(self, "tasks")
+ and self.checkpoint_pending_writes
+ and any(task.writes for task in self.tasks.values())
+ ):
+ updated_channels = apply_writes(
+ self.checkpoint,
+ self.channels,
+ self.tasks.values(),
+ self.checkpointer_get_next_version,
+ self.trigger_to_nodes,
+ )
+ if not updated_channels.isdisjoint(
+ (self.output_keys,)
+ if isinstance(self.output_keys, str)
+ else self.output_keys
+ ):
+ self._emit(
+ "values",
+ map_output_values,
+ self.output_keys,
+ [w for t in self.tasks.values() for w in t.writes],
+ self.channels,
+ )
+ # emit INTERRUPT if exception is empty (otherwise emitted by put_writes)
+ if exc_value is not None and (not exc_value.args or not exc_value.args[0]):
+ self._emit(
+ "updates",
+ lambda: iter(
+ [{INTERRUPT: cast(GraphInterrupt, exc_value).args[0]}]
+ ),
+ )
+ # save final output
+ self.output = read_channels(self.channels, self.output_keys)
+ # suppress interrupt
+ return True
+ elif exc_type is None:
+ # save final output
+ self.output = read_channels(self.channels, self.output_keys)
+
+ def _emit(
+ self,
+ mode: StreamMode,
+ values: Callable[P, Iterator[Any]],
+ *args: P.args,
+ **kwargs: P.kwargs,
+ ) -> None:
+ if self.stream is None:
+ return
+ debug_remap = mode in ("checkpoints", "tasks") and "debug" in self.stream.modes
+ if mode not in self.stream.modes and not debug_remap:
+ return
+ for v in values(*args, **kwargs):
+ if mode in self.stream.modes:
+ self.stream((self.checkpoint_ns, mode, v))
+ # "debug" mode is "checkpoints" or "tasks" with a wrapper dict
+ if debug_remap:
+ self.stream(
+ (
+ self.checkpoint_ns,
+ "debug",
+ {
+ "step": self.step - 1
+ if mode == "checkpoints"
+ else self.step,
+ "timestamp": datetime.now(timezone.utc).isoformat(),
+ "type": "checkpoint"
+ if mode == "checkpoints"
+ else "task_result"
+ if "result" in v
+ else "task",
+ "payload": v,
+ },
+ )
+ )
+
+ def output_writes(
+ self, task_id: str, writes: WritesT, *, cached: bool = False
+ ) -> None:
+ if task := self.tasks.get(task_id):
+ if task.config is not None and TAG_HIDDEN in task.config.get(
+ "tags", EMPTY_SEQ
+ ):
+ return
+ if writes[0][0] == INTERRUPT:
+ # in loop.py we append a bool to the PUSH task paths to indicate
+ # whether or not a call was present. If so,
+ # we don't emit the interrupt as it'll be emitted by the parent
+ if task.path[0] == PUSH and task.path[-1] is True:
+ return
+ interrupts = [
+ {
+ INTERRUPT: tuple(
+ v
+ for w in writes
+ if w[0] == INTERRUPT
+ for v in (w[1] if isinstance(w[1], Sequence) else (w[1],))
+ )
+ }
+ ]
+ stream_modes = self.stream.modes if self.stream else []
+ if "updates" in stream_modes:
+ self._emit("updates", lambda: iter(interrupts))
+ if "values" in stream_modes:
+ current_values = read_channels(self.channels, self.output_keys)
+ # self.output_keys is a sequence, stream chunk contains entire state and interrupts
+ if isinstance(current_values, dict):
+ current_values[INTERRUPT] = interrupts[0][INTERRUPT]
+ self._emit("values", lambda: iter([current_values]))
+ # self.output_keys is a string, stream chunk contains only interrupts
+ else:
+ self._emit("values", lambda: iter(interrupts))
+ elif writes[0][0] != ERROR:
+ self._emit(
+ "updates",
+ map_output_updates,
+ self.output_keys,
+ [(task, writes)],
+ cached,
+ )
+ if not cached:
+ self._emit(
+ "tasks",
+ map_debug_task_results,
+ (task, writes),
+ self.stream_keys,
+ )
+
+
+class SyncPregelLoop(PregelLoop, AbstractContextManager):
+ def __init__(
+ self,
+ input: Any | None,
+ *,
+ stream: StreamProtocol | None,
+ config: RunnableConfig,
+ store: BaseStore | None,
+ cache: BaseCache | None,
+ checkpointer: BaseCheckpointSaver | None,
+ nodes: Mapping[str, PregelNode],
+ specs: Mapping[str, BaseChannel | ManagedValueSpec],
+ trigger_to_nodes: Mapping[str, Sequence[str]],
+ durability: Durability,
+ manager: None | AsyncParentRunManager | ParentRunManager = None,
+ interrupt_after: All | Sequence[str] = EMPTY_SEQ,
+ interrupt_before: All | Sequence[str] = EMPTY_SEQ,
+ input_keys: str | Sequence[str] = EMPTY_SEQ,
+ output_keys: str | Sequence[str] = EMPTY_SEQ,
+ stream_keys: str | Sequence[str] = EMPTY_SEQ,
+ migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
+ retry_policy: Sequence[RetryPolicy] = (),
+ cache_policy: CachePolicy | None = None,
+ ) -> None:
+ super().__init__(
+ input,
+ stream=stream,
+ config=config,
+ checkpointer=checkpointer,
+ cache=cache,
+ store=store,
+ nodes=nodes,
+ specs=specs,
+ input_keys=input_keys,
+ output_keys=output_keys,
+ stream_keys=stream_keys,
+ interrupt_after=interrupt_after,
+ interrupt_before=interrupt_before,
+ manager=manager,
+ migrate_checkpoint=migrate_checkpoint,
+ trigger_to_nodes=trigger_to_nodes,
+ retry_policy=retry_policy,
+ cache_policy=cache_policy,
+ durability=durability,
+ )
+ self.stack = ExitStack()
+ if checkpointer:
+ self.checkpointer_get_next_version = checkpointer.get_next_version
+ self.checkpointer_put_writes = checkpointer.put_writes
+ self.checkpointer_put_writes_accepts_task_path = (
+ signature(checkpointer.put_writes).parameters.get("task_path")
+ is not None
+ )
+ else:
+ self.checkpointer_get_next_version = increment
+ self._checkpointer_put_after_previous = None # type: ignore[assignment]
+ self.checkpointer_put_writes = None
+ self.checkpointer_put_writes_accepts_task_path = False
+
+ def _checkpointer_put_after_previous(
+ self,
+ prev: concurrent.futures.Future | None,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ try:
+ if prev is not None:
+ prev.result()
+ finally:
+ cast(BaseCheckpointSaver, self.checkpointer).put(
+ config, checkpoint, metadata, new_versions
+ )
+
+ def match_cached_writes(self) -> Sequence[PregelExecutableTask]:
+ if self.cache is None:
+ return ()
+ matched: list[PregelExecutableTask] = []
+ if cached := {
+ (t.cache_key.ns, t.cache_key.key): t
+ for t in self.tasks.values()
+ if t.cache_key and not t.writes
+ }:
+ for key, values in self.cache.get(tuple(cached)).items():
+ task = cached[key]
+ task.writes.extend(values)
+ matched.append(task)
+ return matched
+
+ def accept_push(
+ self, task: PregelExecutableTask, write_idx: int, call: Call | None = None
+ ) -> PregelExecutableTask | None:
+ if pushed := super().accept_push(task, write_idx, call):
+ for task in self.match_cached_writes():
+ self.output_writes(task.id, task.writes, cached=True)
+ return pushed
+
+ def put_writes(self, task_id: str, writes: WritesT) -> None:
+ """Put writes for a task, to be read by the next tick."""
+ super().put_writes(task_id, writes)
+ if not writes or self.cache is None or not hasattr(self, "tasks"):
+ return
+ task = self.tasks.get(task_id)
+ if task is None or task.cache_key is None:
+ return
+ self.submit(
+ self.cache.set,
+ {
+ (task.cache_key.ns, task.cache_key.key): (
+ task.writes,
+ task.cache_key.ttl,
+ )
+ },
+ )
+
+ # context manager
+
+ def __enter__(self) -> Self:
+ if self.checkpointer:
+ saved = self.checkpointer.get_tuple(self.checkpoint_config)
+ else:
+ saved = None
+ if saved is None:
+ saved = CheckpointTuple(
+ self.checkpoint_config, empty_checkpoint(), {"step": -2}, None, []
+ )
+ elif self._migrate_checkpoint is not None:
+ self._migrate_checkpoint(saved.checkpoint)
+ self.checkpoint_config = {
+ **self.checkpoint_config,
+ **saved.config,
+ CONF: {
+ CONFIG_KEY_CHECKPOINT_NS: "",
+ **self.checkpoint_config.get(CONF, {}),
+ **saved.config.get(CONF, {}),
+ },
+ }
+ self.prev_checkpoint_config = saved.parent_config
+ self.checkpoint_id_saved = saved.checkpoint["id"]
+ self.checkpoint = saved.checkpoint
+ self.checkpoint_metadata = saved.metadata
+ self.checkpoint_pending_writes = (
+ [(str(tid), k, v) for tid, k, v in saved.pending_writes]
+ if saved.pending_writes is not None
+ else []
+ )
+
+ self.submit = self.stack.enter_context(BackgroundExecutor(self.config))
+ self.channels, self.managed = channels_from_checkpoint(
+ self.specs, self.checkpoint
+ )
+ self.stack.push(self._suppress_interrupt)
+ self.status = "input"
+ self.step = self.checkpoint_metadata["step"] + 1
+ self.stop = self.step + self.config["recursion_limit"] + 1
+ self.checkpoint_previous_versions = self.checkpoint["channel_versions"].copy()
+ self.updated_channels = self._first(
+ input_keys=self.input_keys,
+ updated_channels=set(self.checkpoint.get("updated_channels")) # type: ignore[arg-type]
+ if self.checkpoint.get("updated_channels")
+ else None,
+ )
+
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> bool | None:
+ # unwind stack
+ return self.stack.__exit__(exc_type, exc_value, traceback)
+
+
+class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
+ def __init__(
+ self,
+ input: Any | None,
+ *,
+ stream: StreamProtocol | None,
+ config: RunnableConfig,
+ store: BaseStore | None,
+ cache: BaseCache | None,
+ checkpointer: BaseCheckpointSaver | None,
+ nodes: Mapping[str, PregelNode],
+ specs: Mapping[str, BaseChannel | ManagedValueSpec],
+ trigger_to_nodes: Mapping[str, Sequence[str]],
+ durability: Durability,
+ interrupt_after: All | Sequence[str] = EMPTY_SEQ,
+ interrupt_before: All | Sequence[str] = EMPTY_SEQ,
+ manager: None | AsyncParentRunManager | ParentRunManager = None,
+ input_keys: str | Sequence[str] = EMPTY_SEQ,
+ output_keys: str | Sequence[str] = EMPTY_SEQ,
+ stream_keys: str | Sequence[str] = EMPTY_SEQ,
+ migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
+ retry_policy: Sequence[RetryPolicy] = (),
+ cache_policy: CachePolicy | None = None,
+ ) -> None:
+ super().__init__(
+ input,
+ stream=stream,
+ config=config,
+ checkpointer=checkpointer,
+ cache=cache,
+ store=store,
+ nodes=nodes,
+ specs=specs,
+ input_keys=input_keys,
+ output_keys=output_keys,
+ stream_keys=stream_keys,
+ interrupt_after=interrupt_after,
+ interrupt_before=interrupt_before,
+ manager=manager,
+ migrate_checkpoint=migrate_checkpoint,
+ trigger_to_nodes=trigger_to_nodes,
+ retry_policy=retry_policy,
+ cache_policy=cache_policy,
+ durability=durability,
+ )
+ self.stack = AsyncExitStack()
+ if checkpointer:
+ self.checkpointer_get_next_version = checkpointer.get_next_version
+ self.checkpointer_put_writes = checkpointer.aput_writes
+ self.checkpointer_put_writes_accepts_task_path = (
+ signature(checkpointer.aput_writes).parameters.get("task_path")
+ is not None
+ )
+ else:
+ self.checkpointer_get_next_version = increment
+ self._checkpointer_put_after_previous = None # type: ignore[assignment]
+ self.checkpointer_put_writes = None
+ self.checkpointer_put_writes_accepts_task_path = False
+
+ async def _checkpointer_put_after_previous(
+ self,
+ prev: asyncio.Task | None,
+ config: RunnableConfig,
+ checkpoint: Checkpoint,
+ metadata: CheckpointMetadata,
+ new_versions: ChannelVersions,
+ ) -> RunnableConfig:
+ try:
+ if prev is not None:
+ await prev
+ finally:
+ await cast(BaseCheckpointSaver, self.checkpointer).aput(
+ config, checkpoint, metadata, new_versions
+ )
+
+ async def amatch_cached_writes(self) -> Sequence[PregelExecutableTask]:
+ if self.cache is None:
+ return []
+ matched: list[PregelExecutableTask] = []
+ if cached := {
+ (t.cache_key.ns, t.cache_key.key): t
+ for t in self.tasks.values()
+ if t.cache_key and not t.writes
+ }:
+ for key, values in (await self.cache.aget(tuple(cached))).items():
+ task = cached[key]
+ task.writes.extend(values)
+ matched.append(task)
+ return matched
+
+ async def aaccept_push(
+ self, task: PregelExecutableTask, write_idx: int, call: Call | None = None
+ ) -> PregelExecutableTask | None:
+ if pushed := super().accept_push(task, write_idx, call):
+ for task in await self.amatch_cached_writes():
+ self.output_writes(task.id, task.writes, cached=True)
+ return pushed
+
+ def put_writes(self, task_id: str, writes: WritesT) -> None:
+ """Put writes for a task, to be read by the next tick."""
+ super().put_writes(task_id, writes)
+ if not writes or self.cache is None or not hasattr(self, "tasks"):
+ return
+ task = self.tasks.get(task_id)
+ if task is None or task.cache_key is None:
+ return
+ if writes[0][0] in (INTERRUPT, ERROR):
+ # only cache successful tasks
+ return
+ self.submit(
+ self.cache.aset,
+ {
+ (task.cache_key.ns, task.cache_key.key): (
+ task.writes,
+ task.cache_key.ttl,
+ )
+ },
+ )
+
+ # context manager
+
+ async def __aenter__(self) -> Self:
+ if self.checkpointer:
+ saved = await self.checkpointer.aget_tuple(self.checkpoint_config)
+ else:
+ saved = None
+ if saved is None:
+ saved = CheckpointTuple(
+ self.checkpoint_config, empty_checkpoint(), {"step": -2}, None, []
+ )
+ elif self._migrate_checkpoint is not None:
+ self._migrate_checkpoint(saved.checkpoint)
+ self.checkpoint_config = {
+ **self.checkpoint_config,
+ **saved.config,
+ CONF: {
+ CONFIG_KEY_CHECKPOINT_NS: "",
+ **self.checkpoint_config.get(CONF, {}),
+ **saved.config.get(CONF, {}),
+ },
+ }
+ self.prev_checkpoint_config = saved.parent_config
+ self.checkpoint_id_saved = saved.checkpoint["id"]
+ self.checkpoint = saved.checkpoint
+ self.checkpoint_metadata = saved.metadata
+ self.checkpoint_pending_writes = (
+ [(str(tid), k, v) for tid, k, v in saved.pending_writes]
+ if saved.pending_writes is not None
+ else []
+ )
+
+ self.submit = await self.stack.enter_async_context(
+ AsyncBackgroundExecutor(self.config)
+ )
+ self.channels, self.managed = channels_from_checkpoint(
+ self.specs, self.checkpoint
+ )
+ self.stack.push(self._suppress_interrupt)
+ self.status = "input"
+ self.step = self.checkpoint_metadata["step"] + 1
+ self.stop = self.step + self.config["recursion_limit"] + 1
+ self.checkpoint_previous_versions = self.checkpoint["channel_versions"].copy()
+ self.updated_channels = self._first(
+ input_keys=self.input_keys,
+ updated_channels=set(self.checkpoint.get("updated_channels")) # type: ignore[arg-type]
+ if self.checkpoint.get("updated_channels")
+ else None,
+ )
+
+ return self
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> bool | None:
+ # unwind stack
+ exit_task = asyncio.create_task(
+ self.stack.__aexit__(exc_type, exc_value, traceback)
+ )
+ try:
+ return await exit_task
+ except asyncio.CancelledError as e:
+ # Bubble up the exit task upon cancellation to permit the API
+ # consumer to await it before e.g., reusing the DB connection.
+ e.args = (*e.args, exit_task)
+ raise
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_messages.py b/langgraph/source/libs/langgraph/langgraph/pregel/_messages.py
new file mode 100644
index 0000000000000000000000000000000000000000..acab06098e0e51f849c3646721b5a127ce6ca1ee
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/_messages.py
@@ -0,0 +1,250 @@
+from __future__ import annotations
+
+from collections.abc import AsyncIterator, Callable, Iterator, Sequence
+from dataclasses import fields, is_dataclass
+from typing import (
+ Any,
+ TypeVar,
+ cast,
+)
+from uuid import UUID, uuid4
+
+from langchain_core.callbacks import BaseCallbackHandler
+from langchain_core.messages import BaseMessage
+from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, LLMResult
+from pydantic import BaseModel
+
+from langgraph._internal._constants import NS_SEP
+from langgraph.constants import TAG_HIDDEN, TAG_NOSTREAM
+from langgraph.pregel.protocol import StreamChunk
+from langgraph.types import Command
+
+try:
+ from langchain_core.tracers._streaming import _StreamingCallbackHandler
+except ImportError:
+ _StreamingCallbackHandler = object # type: ignore
+
+T = TypeVar("T")
+Meta = tuple[tuple[str, ...], dict[str, Any]]
+
+
+def _state_values(obj: Any) -> Sequence[Any]:
+ """Extract top-level field values from a state object (dict, BaseModel, or dataclass)."""
+ if isinstance(obj, dict):
+ return list(obj.values())
+ elif isinstance(obj, BaseModel):
+ return [getattr(obj, k) for k in type(obj).model_fields]
+ elif is_dataclass(obj) and not isinstance(obj, type):
+ return [getattr(obj, f.name) for f in fields(obj)]
+ return ()
+
+
+class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
+ """A callback handler that implements stream_mode=messages.
+
+ Collects messages from:
+ (1) chat model stream events; and
+ (2) node outputs.
+ """
+
+ run_inline = True
+ """We want this callback to run in the main thread to avoid order/locking issues."""
+
+ def __init__(
+ self,
+ stream: Callable[[StreamChunk], None],
+ subgraphs: bool,
+ *,
+ parent_ns: tuple[str, ...] | None = None,
+ ) -> None:
+ """Configure the handler to stream messages from LLMs and nodes.
+
+ Args:
+ stream: A callable that takes a StreamChunk and emits it.
+ subgraphs: Whether to emit messages from subgraphs.
+ parent_ns: The namespace where the handler was created.
+ We keep track of this namespace to allow calls to subgraphs that
+ were explicitly requested as a stream with `messages` mode
+ configured.
+
+ Example:
+ parent_ns is used to handle scenarios where the subgraph is explicitly
+ streamed with `stream_mode="messages"`.
+
+ ```python
+ def parent_graph_node():
+ # This node is in the parent graph.
+ async for event in some_subgraph(..., stream_mode="messages"):
+ do something with event # <-- these events will be emitted
+ return ...
+
+ parent_graph.invoke(subgraphs=False)
+ ```
+ """
+ self.stream = stream
+ self.subgraphs = subgraphs
+ self.metadata: dict[UUID, Meta] = {}
+ self.seen: set[int | str] = set()
+ self.parent_ns = parent_ns
+
+ def _emit(self, meta: Meta, message: BaseMessage, *, dedupe: bool = False) -> None:
+ if dedupe and message.id in self.seen:
+ return
+ else:
+ if message.id is None:
+ message.id = str(uuid4())
+ self.seen.add(message.id)
+ self.stream((meta[0], "messages", (message, meta[1])))
+
+ def _find_and_emit_messages(self, meta: Meta, response: Any) -> None:
+ if isinstance(response, BaseMessage):
+ self._emit(meta, response, dedupe=True)
+ elif isinstance(response, Sequence):
+ for value in response:
+ if isinstance(value, BaseMessage):
+ self._emit(meta, value, dedupe=True)
+ else:
+ for value in _state_values(response):
+ if isinstance(value, BaseMessage):
+ self._emit(meta, value, dedupe=True)
+ elif isinstance(value, Sequence):
+ for item in value:
+ if isinstance(item, BaseMessage):
+ self._emit(meta, item, dedupe=True)
+
+ def tap_output_aiter(
+ self, run_id: UUID, output: AsyncIterator[T]
+ ) -> AsyncIterator[T]:
+ return output
+
+ def tap_output_iter(self, run_id: UUID, output: Iterator[T]) -> Iterator[T]:
+ return output
+
+ def on_chat_model_start(
+ self,
+ serialized: dict[str, Any],
+ messages: list[list[BaseMessage]],
+ *,
+ run_id: UUID,
+ parent_run_id: UUID | None = None,
+ tags: list[str] | None = None,
+ metadata: dict[str, Any] | None = None,
+ **kwargs: Any,
+ ) -> Any:
+ if metadata and (not tags or (TAG_NOSTREAM not in tags)):
+ ns = tuple(cast(str, metadata["langgraph_checkpoint_ns"]).split(NS_SEP))[
+ :-1
+ ]
+ if not self.subgraphs and len(ns) > 0 and ns != self.parent_ns:
+ return
+ if tags:
+ if filtered_tags := [t for t in tags if not t.startswith("seq:step")]:
+ metadata["tags"] = filtered_tags
+ self.metadata[run_id] = (ns, metadata)
+
+ def on_llm_new_token(
+ self,
+ token: str,
+ *,
+ chunk: ChatGenerationChunk | None = None,
+ run_id: UUID,
+ parent_run_id: UUID | None = None,
+ tags: list[str] | None = None,
+ **kwargs: Any,
+ ) -> Any:
+ if not isinstance(chunk, ChatGenerationChunk):
+ return
+ if meta := self.metadata.get(run_id):
+ self._emit(meta, chunk.message)
+
+ def on_llm_end(
+ self,
+ response: LLMResult,
+ *,
+ run_id: UUID,
+ parent_run_id: UUID | None = None,
+ **kwargs: Any,
+ ) -> Any:
+ if meta := self.metadata.get(run_id):
+ if response.generations and response.generations[0]:
+ gen = response.generations[0][0]
+ if isinstance(gen, ChatGeneration):
+ self._emit(meta, gen.message, dedupe=True)
+ self.metadata.pop(run_id, None)
+
+ def on_llm_error(
+ self,
+ error: BaseException,
+ *,
+ run_id: UUID,
+ parent_run_id: UUID | None = None,
+ **kwargs: Any,
+ ) -> Any:
+ self.metadata.pop(run_id, None)
+
+ def on_chain_start(
+ self,
+ serialized: dict[str, Any],
+ inputs: dict[str, Any],
+ *,
+ run_id: UUID,
+ parent_run_id: UUID | None = None,
+ tags: list[str] | None = None,
+ metadata: dict[str, Any] | None = None,
+ **kwargs: Any,
+ ) -> Any:
+ if (
+ metadata
+ and kwargs.get("name") == metadata.get("langgraph_node")
+ and (not tags or TAG_HIDDEN not in tags)
+ ):
+ ns = tuple(cast(str, metadata["langgraph_checkpoint_ns"]).split(NS_SEP))[
+ :-1
+ ]
+ if not self.subgraphs and len(ns) > 0:
+ return
+ self.metadata[run_id] = (ns, metadata)
+ for value in _state_values(inputs):
+ if isinstance(value, BaseMessage):
+ if value.id is not None:
+ self.seen.add(value.id)
+ elif isinstance(value, Sequence) and not isinstance(value, str):
+ for item in value:
+ if isinstance(item, BaseMessage):
+ if item.id is not None:
+ self.seen.add(item.id)
+
+ def on_chain_end(
+ self,
+ response: Any,
+ *,
+ run_id: UUID,
+ parent_run_id: UUID | None = None,
+ **kwargs: Any,
+ ) -> Any:
+ if meta := self.metadata.pop(run_id, None):
+ # Handle Command node updates
+ if isinstance(response, Command):
+ self._find_and_emit_messages(meta, response.update)
+ # Handle list of Command updates
+ elif isinstance(response, Sequence) and any(
+ isinstance(value, Command) for value in response
+ ):
+ for value in response:
+ if isinstance(value, Command):
+ self._find_and_emit_messages(meta, value.update)
+ else:
+ self._find_and_emit_messages(meta, value)
+ # Handle basic updates / streaming
+ else:
+ self._find_and_emit_messages(meta, response)
+
+ def on_chain_error(
+ self,
+ error: BaseException,
+ *,
+ run_id: UUID,
+ parent_run_id: UUID | None = None,
+ **kwargs: Any,
+ ) -> Any:
+ self.metadata.pop(run_id, None)
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_read.py b/langgraph/source/libs/langgraph/langgraph/pregel/_read.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d4c211356fb68c6ec833529b52668a827039e22
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/_read.py
@@ -0,0 +1,277 @@
+from __future__ import annotations
+
+from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence
+from functools import cached_property
+from typing import (
+ Any,
+)
+
+from langchain_core.runnables import Runnable, RunnableConfig
+
+from langgraph._internal._config import merge_configs
+from langgraph._internal._constants import CONF, CONFIG_KEY_READ
+from langgraph._internal._runnable import RunnableCallable, RunnableSeq
+from langgraph.pregel._utils import find_subgraph_pregel
+from langgraph.pregel._write import ChannelWrite
+from langgraph.pregel.protocol import PregelProtocol
+from langgraph.types import CachePolicy, RetryPolicy
+
+READ_TYPE = Callable[[str | Sequence[str], bool], Any | dict[str, Any]]
+INPUT_CACHE_KEY_TYPE = tuple[Callable[..., Any], tuple[str, ...]]
+
+
+class ChannelRead(RunnableCallable):
+ """Implements the logic for reading state from CONFIG_KEY_READ.
+ Usable both as a runnable as well as a static method to call imperatively."""
+
+ channel: str | list[str]
+
+ fresh: bool = False
+
+ mapper: Callable[[Any], Any] | None = None
+
+ def __init__(
+ self,
+ channel: str | list[str],
+ *,
+ fresh: bool = False,
+ mapper: Callable[[Any], Any] | None = None,
+ tags: list[str] | None = None,
+ ) -> None:
+ super().__init__(
+ func=self._read,
+ afunc=self._aread,
+ tags=tags,
+ name=None,
+ trace=False,
+ )
+ self.fresh = fresh
+ self.mapper = mapper
+ self.channel = channel
+
+ def get_name(self, suffix: str | None = None, *, name: str | None = None) -> str:
+ if name:
+ pass
+ elif isinstance(self.channel, str):
+ name = f"ChannelRead<{self.channel}>"
+ else:
+ name = f"ChannelRead<{','.join(self.channel)}>"
+ return super().get_name(suffix, name=name)
+
+ def _read(self, _: Any, config: RunnableConfig) -> Any:
+ return self.do_read(
+ config, select=self.channel, fresh=self.fresh, mapper=self.mapper
+ )
+
+ async def _aread(self, _: Any, config: RunnableConfig) -> Any:
+ return self.do_read(
+ config, select=self.channel, fresh=self.fresh, mapper=self.mapper
+ )
+
+ @staticmethod
+ def do_read(
+ config: RunnableConfig,
+ *,
+ select: str | list[str],
+ fresh: bool = False,
+ mapper: Callable[[Any], Any] | None = None,
+ ) -> Any:
+ try:
+ read: READ_TYPE = config[CONF][CONFIG_KEY_READ]
+ except KeyError:
+ raise RuntimeError(
+ "Not configured with a read function"
+ "Make sure to call in the context of a Pregel process"
+ )
+ if mapper:
+ return mapper(read(select, fresh))
+ else:
+ return read(select, fresh)
+
+
+DEFAULT_BOUND = RunnableCallable(lambda input: input)
+
+
+class PregelNode:
+ """A node in a Pregel graph. This won't be invoked as a runnable by the graph
+ itself, but instead acts as a container for the components necessary to make
+ a PregelExecutableTask for a node."""
+
+ channels: str | list[str]
+ """The channels that will be passed as input to `bound`.
+ If a str, the node will be invoked with its value if it isn't empty.
+ If a list, the node will be invoked with a dict of those channels' values."""
+
+ triggers: list[str]
+ """If any of these channels is written to, this node will be triggered in
+ the next step."""
+
+ mapper: Callable[[Any], Any] | None
+ """A function to transform the input before passing it to `bound`."""
+
+ writers: list[Runnable]
+ """A list of writers that will be executed after `bound`, responsible for
+ taking the output of `bound` and writing it to the appropriate channels."""
+
+ bound: Runnable[Any, Any]
+ """The main logic of the node. This will be invoked with the input from
+ `channels`."""
+
+ retry_policy: Sequence[RetryPolicy] | None
+ """The retry policies to use when invoking the node."""
+
+ cache_policy: CachePolicy | None
+ """The cache policy to use when invoking the node."""
+
+ tags: Sequence[str] | None
+ """Tags to attach to the node for tracing."""
+
+ metadata: Mapping[str, Any] | None
+ """Metadata to attach to the node for tracing."""
+
+ subgraphs: Sequence[PregelProtocol]
+ """Subgraphs used by the node."""
+
+ def __init__(
+ self,
+ *,
+ channels: str | list[str],
+ triggers: Sequence[str],
+ mapper: Callable[[Any], Any] | None = None,
+ writers: list[Runnable] | None = None,
+ tags: list[str] | None = None,
+ metadata: Mapping[str, Any] | None = None,
+ bound: Runnable[Any, Any] | None = None,
+ retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
+ cache_policy: CachePolicy | None = None,
+ subgraphs: Sequence[PregelProtocol] | None = None,
+ ) -> None:
+ self.channels = channels
+ self.triggers = list(triggers)
+ self.mapper = mapper
+ self.writers = writers or []
+ self.bound = bound if bound is not None else DEFAULT_BOUND
+ self.cache_policy = cache_policy
+ if isinstance(retry_policy, RetryPolicy):
+ self.retry_policy = (retry_policy,)
+ else:
+ self.retry_policy = retry_policy
+ self.tags = tags
+ self.metadata = metadata
+ if subgraphs is not None:
+ self.subgraphs = subgraphs
+ elif self.bound is not DEFAULT_BOUND:
+ try:
+ subgraph = find_subgraph_pregel(self.bound)
+ except Exception:
+ subgraph = None
+ if subgraph:
+ self.subgraphs = [subgraph]
+ else:
+ self.subgraphs = []
+ else:
+ self.subgraphs = []
+
+ def copy(self, update: dict[str, Any]) -> PregelNode:
+ attrs = {**self.__dict__, **update}
+ # Drop the cached properties
+ attrs.pop("flat_writers", None)
+ attrs.pop("node", None)
+ attrs.pop("input_cache_key", None)
+ return PregelNode(**attrs)
+
+ @cached_property
+ def flat_writers(self) -> list[Runnable]:
+ """Get writers with optimizations applied. Dedupes consecutive ChannelWrites."""
+ writers = self.writers.copy()
+ while (
+ len(writers) > 1
+ and isinstance(writers[-1], ChannelWrite)
+ and isinstance(writers[-2], ChannelWrite)
+ ):
+ # we can combine writes if they are consecutive
+ # careful to not modify the original writers list or ChannelWrite
+ writers[-2] = ChannelWrite(
+ writes=writers[-2].writes + writers[-1].writes,
+ )
+ writers.pop()
+ return writers
+
+ @cached_property
+ def node(self) -> Runnable[Any, Any] | None:
+ """Get a runnable that combines `bound` and `writers`."""
+ writers = self.flat_writers
+ if self.bound is DEFAULT_BOUND and not writers:
+ return None
+ elif self.bound is DEFAULT_BOUND and len(writers) == 1:
+ return writers[0]
+ elif self.bound is DEFAULT_BOUND:
+ return RunnableSeq(*writers)
+ elif writers:
+ return RunnableSeq(self.bound, *writers)
+ else:
+ return self.bound
+
+ @cached_property
+ def input_cache_key(self) -> INPUT_CACHE_KEY_TYPE:
+ """Get a cache key for the input to the node.
+ This is used to avoid calculating the same input multiple times."""
+ return (
+ self.mapper,
+ tuple(self.channels)
+ if isinstance(self.channels, list)
+ else (self.channels,),
+ )
+
+ def invoke(
+ self,
+ input: Any,
+ config: RunnableConfig | None = None,
+ **kwargs: Any | None,
+ ) -> Any:
+ self_config: RunnableConfig = {"metadata": self.metadata, "tags": self.tags}
+ return self.bound.invoke(
+ input,
+ merge_configs(self_config, config),
+ **kwargs,
+ )
+
+ async def ainvoke(
+ self,
+ input: Any,
+ config: RunnableConfig | None = None,
+ **kwargs: Any | None,
+ ) -> Any:
+ self_config: RunnableConfig = {"metadata": self.metadata, "tags": self.tags}
+ return await self.bound.ainvoke(
+ input,
+ merge_configs(self_config, config),
+ **kwargs,
+ )
+
+ def stream(
+ self,
+ input: Any,
+ config: RunnableConfig | None = None,
+ **kwargs: Any | None,
+ ) -> Iterator[Any]:
+ self_config: RunnableConfig = {"metadata": self.metadata, "tags": self.tags}
+ yield from self.bound.stream(
+ input,
+ merge_configs(self_config, config),
+ **kwargs,
+ )
+
+ async def astream(
+ self,
+ input: Any,
+ config: RunnableConfig | None = None,
+ **kwargs: Any | None,
+ ) -> AsyncIterator[Any]:
+ self_config: RunnableConfig = {"metadata": self.metadata, "tags": self.tags}
+ async for item in self.bound.astream(
+ input,
+ merge_configs(self_config, config),
+ **kwargs,
+ ):
+ yield item
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_retry.py b/langgraph/source/libs/langgraph/langgraph/pregel/_retry.py
new file mode 100644
index 0000000000000000000000000000000000000000..b42b636443e13746df00546a02d67231003ffcbd
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/_retry.py
@@ -0,0 +1,217 @@
+from __future__ import annotations
+
+import asyncio
+import logging
+import random
+import sys
+import time
+from collections.abc import Awaitable, Callable, Sequence
+from dataclasses import replace
+from typing import Any
+
+from langgraph._internal._config import patch_configurable, recast_checkpoint_ns
+from langgraph._internal._constants import (
+ CONF,
+ CONFIG_KEY_CHECKPOINT_NS,
+ CONFIG_KEY_RESUMING,
+ NS_SEP,
+)
+from langgraph.errors import GraphBubbleUp, ParentCommand
+from langgraph.types import Command, PregelExecutableTask, RetryPolicy
+
+logger = logging.getLogger(__name__)
+SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11)
+
+
+def run_with_retry(
+ task: PregelExecutableTask,
+ retry_policy: Sequence[RetryPolicy] | None,
+ configurable: dict[str, Any] | None = None,
+) -> None:
+ """Run a task with retries."""
+ retry_policy = task.retry_policy or retry_policy
+ attempts = 0
+ config = task.config
+ if configurable is not None:
+ config = patch_configurable(config, configurable)
+ while True:
+ try:
+ # clear any writes from previous attempts
+ task.writes.clear()
+ # run the task
+ return task.proc.invoke(task.input, config)
+ except ParentCommand as exc:
+ ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]
+ cmd = exc.args[0]
+ # strip task_ids from namespace for comparison (ns format: "node1|node2:task_id")
+ if cmd.graph in (ns, recast_checkpoint_ns(ns), task.name):
+ # 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
+ parts = ns.split(NS_SEP)
+ if parts[-1].isdigit():
+ parts.pop()
+ parent_ns = NS_SEP.join(parts[:-1])
+ exc.args = (replace(cmd, graph=parent_ns),)
+ # bubble up
+ raise
+ except GraphBubbleUp:
+ # 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 not retry_policy:
+ raise
+
+ # Check which retry policy applies to this exception
+ matching_policy = None
+ for policy in retry_policy:
+ if _should_retry_on(policy, exc):
+ matching_policy = policy
+ break
+
+ if not matching_policy:
+ raise
+
+ # increment attempts
+ attempts += 1
+ # check if we should give up
+ if attempts >= matching_policy.max_attempts:
+ raise
+ # sleep before retrying
+ interval = matching_policy.initial_interval
+ # Apply backoff factor based on attempt count
+ interval = min(
+ matching_policy.max_interval,
+ interval * (matching_policy.backoff_factor ** (attempts - 1)),
+ )
+
+ # Apply jitter if configured
+ sleep_time = (
+ interval + random.uniform(0, 1) if matching_policy.jitter else interval
+ )
+ time.sleep(sleep_time)
+
+ # log the retry
+ logger.info(
+ f"Retrying task {task.name} after {sleep_time:.2f} seconds (attempt {attempts}) after {exc.__class__.__name__} {exc}",
+ exc_info=exc,
+ )
+ # signal subgraphs to resume (if available)
+ config = patch_configurable(config, {CONFIG_KEY_RESUMING: True})
+
+
+async def arun_with_retry(
+ task: PregelExecutableTask,
+ retry_policy: Sequence[RetryPolicy] | None,
+ stream: bool = False,
+ match_cached_writes: Callable[[], Awaitable[Sequence[PregelExecutableTask]]]
+ | None = None,
+ configurable: dict[str, Any] | None = None,
+) -> None:
+ """Run a task asynchronously with retries."""
+ retry_policy = task.retry_policy or retry_policy
+ attempts = 0
+ config = task.config
+ if configurable is not None:
+ config = patch_configurable(config, configurable)
+ if match_cached_writes is not None and task.cache_key is not None:
+ for t in await match_cached_writes():
+ if t is task:
+ # if the task is already cached, return
+ return
+ while True:
+ try:
+ # clear any writes from previous attempts
+ task.writes.clear()
+ # run the task
+ if stream:
+ async for _ in task.proc.astream(task.input, config):
+ pass
+ # if successful, end
+ break
+ else:
+ return await task.proc.ainvoke(task.input, config)
+ except ParentCommand as exc:
+ ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]
+ cmd = exc.args[0]
+ # strip task_ids from namespace for comparison (ns format: "node1|node2:task_id")
+ if cmd.graph in (ns, recast_checkpoint_ns(ns), task.name):
+ # 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
+ parts = ns.split(NS_SEP)
+ if parts[-1].isdigit():
+ parts.pop()
+ parent_ns = NS_SEP.join(parts[:-1])
+ exc.args = (replace(cmd, graph=parent_ns),)
+ # bubble up
+ raise
+ except GraphBubbleUp:
+ # 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 not retry_policy:
+ raise
+
+ # Check which retry policy applies to this exception
+ matching_policy = None
+ for policy in retry_policy:
+ if _should_retry_on(policy, exc):
+ matching_policy = policy
+ break
+
+ if not matching_policy:
+ raise
+
+ # increment attempts
+ attempts += 1
+ # check if we should give up
+ if attempts >= matching_policy.max_attempts:
+ raise
+ # sleep before retrying
+ interval = matching_policy.initial_interval
+ # Apply backoff factor based on attempt count
+ interval = min(
+ matching_policy.max_interval,
+ interval * (matching_policy.backoff_factor ** (attempts - 1)),
+ )
+
+ # Apply jitter if configured
+ sleep_time = (
+ interval + random.uniform(0, 1) if matching_policy.jitter else interval
+ )
+ await asyncio.sleep(sleep_time)
+
+ # log the retry
+ logger.info(
+ f"Retrying task {task.name} after {sleep_time:.2f} seconds (attempt {attempts}) after {exc.__class__.__name__} {exc}",
+ exc_info=exc,
+ )
+ # signal subgraphs to resume (if available)
+ config = patch_configurable(config, {CONFIG_KEY_RESUMING: True})
+
+
+def _should_retry_on(retry_policy: RetryPolicy, exc: Exception) -> bool:
+ """Check if the given exception should be retried based on the retry policy."""
+ if isinstance(retry_policy.retry_on, Sequence):
+ return isinstance(exc, tuple(retry_policy.retry_on))
+ elif isinstance(retry_policy.retry_on, type) and issubclass(
+ retry_policy.retry_on, Exception
+ ):
+ return isinstance(exc, retry_policy.retry_on)
+ elif callable(retry_policy.retry_on):
+ return retry_policy.retry_on(exc) # type: ignore[call-arg]
+ else:
+ raise TypeError(
+ "retry_on must be an Exception class, a list or tuple of Exception classes, or a callable"
+ )
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_runner.py b/langgraph/source/libs/langgraph/langgraph/pregel/_runner.py
new file mode 100644
index 0000000000000000000000000000000000000000..f5e8832cb6c7b6eece92a6b9d40ffa4ed4161aa2
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/_runner.py
@@ -0,0 +1,769 @@
+from __future__ import annotations
+
+import asyncio
+import concurrent.futures
+import inspect
+import threading
+import time
+import weakref
+from collections.abc import (
+ AsyncIterator,
+ Awaitable,
+ Callable,
+ Iterable,
+ Iterator,
+ Sequence,
+)
+from functools import partial
+from typing import (
+ Any,
+ Generic,
+ TypeVar,
+ cast,
+)
+
+from langchain_core.callbacks import Callbacks
+
+from langgraph._internal._constants import (
+ CONF,
+ CONFIG_KEY_CALL,
+ CONFIG_KEY_SCRATCHPAD,
+ ERROR,
+ INTERRUPT,
+ NO_WRITES,
+ RESUME,
+ RETURN,
+)
+from langgraph._internal._future import chain_future, run_coroutine_threadsafe
+from langgraph._internal._scratchpad import PregelScratchpad
+from langgraph._internal._typing import MISSING
+from langgraph.constants import TAG_HIDDEN
+from langgraph.errors import GraphBubbleUp, GraphInterrupt
+from langgraph.pregel._algo import Call
+from langgraph.pregel._executor import Submit
+from langgraph.pregel._retry import arun_with_retry, run_with_retry
+from langgraph.types import (
+ CachePolicy,
+ PregelExecutableTask,
+ RetryPolicy,
+)
+
+F = TypeVar("F", concurrent.futures.Future, asyncio.Future)
+E = TypeVar("E", threading.Event, asyncio.Event)
+
+# List of filenames to exclude from exception traceback
+# Note: Frames will be removed if they are the last frame in traceback, recursively
+EXCLUDED_FRAME_FNAMES = (
+ "langgraph/pregel/retry.py",
+ "langgraph/pregel/runner.py",
+ "langgraph/pregel/executor.py",
+ "langgraph/utils/runnable.py",
+ "langchain_core/runnables/config.py",
+ "concurrent/futures/thread.py",
+ "concurrent/futures/_base.py",
+)
+
+SKIP_RERAISE_SET: weakref.WeakSet[concurrent.futures.Future | asyncio.Future] = (
+ weakref.WeakSet()
+)
+
+
+class FuturesDict(Generic[F, E], dict[F, PregelExecutableTask | None]):
+ event: E
+ callback: weakref.ref[Callable[[PregelExecutableTask, BaseException | None], None]]
+ counter: int
+ done: set[F]
+ lock: threading.Lock
+
+ def __init__(
+ self,
+ event: E,
+ callback: weakref.ref[
+ Callable[[PregelExecutableTask, BaseException | None], None]
+ ],
+ future_type: type[F],
+ # used for generic typing, newer py supports FutureDict[...](...)
+ ) -> None:
+ super().__init__()
+ self.lock = threading.Lock()
+ self.event = event
+ self.callback = callback
+ self.counter = 0
+ self.done: set[F] = set()
+
+ def __setitem__(
+ self,
+ key: F,
+ value: PregelExecutableTask | None,
+ ) -> None:
+ super().__setitem__(key, value) # type: ignore[index]
+ if value is not None:
+ with self.lock:
+ self.event.clear()
+ self.counter += 1
+ key.add_done_callback(partial(self.on_done, value))
+
+ def on_done(
+ self,
+ task: PregelExecutableTask,
+ fut: F,
+ ) -> None:
+ try:
+ if cb := self.callback():
+ cb(task, _exception(fut))
+ finally:
+ with self.lock:
+ self.done.add(fut)
+ self.counter -= 1
+ if self.counter == 0 or _should_stop_others(self.done):
+ self.event.set()
+
+
+class PregelRunner:
+ """Responsible for executing a set of Pregel tasks concurrently, committing
+ their writes, yielding control to caller when there is output to emit, and
+ interrupting other tasks if appropriate."""
+
+ def __init__(
+ self,
+ *,
+ submit: weakref.ref[Submit],
+ put_writes: weakref.ref[Callable[[str, Sequence[tuple[str, Any]]], None]],
+ use_astream: bool = False,
+ node_finished: Callable[[str], None] | None = None,
+ ) -> None:
+ self.submit = submit
+ self.put_writes = put_writes
+ self.use_astream = use_astream
+ self.node_finished = node_finished
+
+ def tick(
+ self,
+ tasks: Iterable[PregelExecutableTask],
+ *,
+ reraise: bool = True,
+ timeout: float | None = None,
+ retry_policy: Sequence[RetryPolicy] | None = None,
+ get_waiter: Callable[[], concurrent.futures.Future[None]] | None = None,
+ schedule_task: Callable[
+ [PregelExecutableTask, int, Call | None],
+ PregelExecutableTask | None,
+ ],
+ ) -> Iterator[None]:
+ tasks = tuple(tasks)
+ futures = FuturesDict(
+ callback=weakref.WeakMethod(self.commit),
+ event=threading.Event(),
+ future_type=concurrent.futures.Future,
+ )
+ # give control back to the caller
+ yield
+ # fast path if single task with no timeout and no waiter
+ if len(tasks) == 0:
+ return
+ elif len(tasks) == 1 and timeout is None and get_waiter is None:
+ t = tasks[0]
+ try:
+ run_with_retry(
+ t,
+ retry_policy,
+ configurable={
+ CONFIG_KEY_CALL: partial(
+ _call,
+ weakref.ref(t),
+ retry_policy=retry_policy,
+ futures=weakref.ref(futures),
+ schedule_task=schedule_task,
+ submit=self.submit,
+ ),
+ },
+ )
+ self.commit(t, None)
+ except Exception as exc:
+ self.commit(t, exc)
+ if reraise and futures:
+ # will be re-raised after futures are done
+ fut: concurrent.futures.Future = concurrent.futures.Future()
+ fut.set_exception(exc)
+ futures.done.add(fut)
+ elif reraise:
+ if tb := exc.__traceback__:
+ while tb.tb_next is not None and any(
+ tb.tb_frame.f_code.co_filename.endswith(name)
+ for name in EXCLUDED_FRAME_FNAMES
+ ):
+ tb = tb.tb_next
+ exc.__traceback__ = tb
+ raise
+ if not futures: # maybe `t` scheduled another task
+ return
+ else:
+ tasks = () # don't reschedule this task
+ # add waiter task if requested
+ if get_waiter is not None:
+ futures[get_waiter()] = None
+ # schedule tasks
+ for t in tasks:
+ fut = self.submit()( # type: ignore[misc]
+ run_with_retry,
+ t,
+ retry_policy,
+ configurable={
+ CONFIG_KEY_CALL: partial(
+ _call,
+ weakref.ref(t),
+ retry_policy=retry_policy,
+ futures=weakref.ref(futures),
+ schedule_task=schedule_task,
+ submit=self.submit,
+ ),
+ },
+ __reraise_on_exit__=reraise,
+ )
+ futures[fut] = t
+ # execute tasks, and wait for one to fail or all to finish.
+ # each task is independent from all other concurrent tasks
+ # yield updates/debug output as each task finishes
+ end_time = timeout + time.monotonic() if timeout else None
+ while len(futures) > (1 if get_waiter is not None else 0):
+ done, inflight = concurrent.futures.wait(
+ futures,
+ return_when=concurrent.futures.FIRST_COMPLETED,
+ timeout=(max(0, end_time - time.monotonic()) if end_time else None),
+ )
+ if not done:
+ break # timed out
+ for fut in done:
+ task = futures.pop(fut)
+ if task is None:
+ # waiter task finished, schedule another
+ if inflight and get_waiter is not None:
+ futures[get_waiter()] = None
+ else:
+ # remove references to loop vars
+ del fut, task
+ # maybe stop other tasks
+ if _should_stop_others(done):
+ break
+ # give control back to the caller
+ yield
+ # wait for done callbacks
+ futures.event.wait(
+ timeout=(max(0, end_time - time.monotonic()) if end_time else None)
+ )
+ # give control back to the caller
+ yield
+ # panic on failure or timeout
+ try:
+ _panic_or_proceed(
+ futures.done.union(f for f, t in futures.items() if t is not None),
+ panic=reraise,
+ )
+ except Exception as exc:
+ if tb := exc.__traceback__:
+ while tb.tb_next is not None and any(
+ tb.tb_frame.f_code.co_filename.endswith(name)
+ for name in EXCLUDED_FRAME_FNAMES
+ ):
+ tb = tb.tb_next
+ exc.__traceback__ = tb
+ raise
+
+ async def atick(
+ self,
+ tasks: Iterable[PregelExecutableTask],
+ *,
+ reraise: bool = True,
+ timeout: float | None = None,
+ retry_policy: Sequence[RetryPolicy] | None = None,
+ get_waiter: Callable[[], asyncio.Future[None]] | None = None,
+ schedule_task: Callable[
+ [PregelExecutableTask, int, Call | None],
+ Awaitable[PregelExecutableTask | None],
+ ],
+ ) -> AsyncIterator[None]:
+ try:
+ loop = asyncio.get_event_loop()
+ except RuntimeError:
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ tasks = tuple(tasks)
+ futures = FuturesDict(
+ callback=weakref.WeakMethod(self.commit),
+ event=asyncio.Event(),
+ future_type=asyncio.Future,
+ )
+ # give control back to the caller
+ yield
+ # fast path if single task with no waiter and no timeout
+ if len(tasks) == 0:
+ return
+ elif len(tasks) == 1 and get_waiter is None and timeout is None:
+ t = tasks[0]
+ try:
+ await arun_with_retry(
+ t,
+ retry_policy,
+ stream=self.use_astream,
+ configurable={
+ CONFIG_KEY_CALL: partial(
+ _acall,
+ weakref.ref(t),
+ stream=self.use_astream,
+ retry_policy=retry_policy,
+ futures=weakref.ref(futures),
+ schedule_task=schedule_task,
+ submit=self.submit,
+ loop=loop,
+ ),
+ },
+ )
+ self.commit(t, None)
+ except Exception as exc:
+ self.commit(t, exc)
+ if reraise and futures:
+ # will be re-raised after futures are done
+ fut: asyncio.Future = loop.create_future()
+ fut.set_exception(exc)
+ futures.done.add(fut)
+ elif reraise:
+ if tb := exc.__traceback__:
+ while tb.tb_next is not None and any(
+ tb.tb_frame.f_code.co_filename.endswith(name)
+ for name in EXCLUDED_FRAME_FNAMES
+ ):
+ tb = tb.tb_next
+ exc.__traceback__ = tb
+ raise
+ if not futures: # maybe `t` scheduled another task
+ return
+ else:
+ tasks = () # don't reschedule this task
+ # add waiter task if requested
+ if get_waiter is not None:
+ futures[get_waiter()] = None
+ # schedule tasks
+ for t in tasks:
+ fut = cast(
+ asyncio.Future,
+ self.submit()( # type: ignore[misc]
+ arun_with_retry,
+ t,
+ retry_policy,
+ stream=self.use_astream,
+ configurable={
+ CONFIG_KEY_CALL: partial(
+ _acall,
+ weakref.ref(t),
+ retry_policy=retry_policy,
+ stream=self.use_astream,
+ futures=weakref.ref(futures),
+ schedule_task=schedule_task,
+ submit=self.submit,
+ loop=loop,
+ ),
+ },
+ __name__=t.name,
+ __cancel_on_exit__=True,
+ __reraise_on_exit__=reraise,
+ ),
+ )
+ futures[fut] = t
+ # execute tasks, and wait for one to fail or all to finish.
+ # each task is independent from all other concurrent tasks
+ # yield updates/debug output as each task finishes
+ end_time = timeout + loop.time() if timeout else None
+ while len(futures) > (1 if get_waiter is not None else 0):
+ done, inflight = await asyncio.wait(
+ futures,
+ return_when=asyncio.FIRST_COMPLETED,
+ timeout=(max(0, end_time - loop.time()) if end_time else None),
+ )
+ if not done:
+ break # timed out
+ for fut in done:
+ task = futures.pop(fut)
+ if task is None:
+ # waiter task finished, schedule another
+ if inflight and get_waiter is not None:
+ futures[get_waiter()] = None
+ else:
+ # remove references to loop vars
+ del fut, task
+ # maybe stop other tasks
+ if _should_stop_others(done):
+ break
+ # give control back to the caller
+ yield
+ # wait for done callbacks
+ await asyncio.wait_for(
+ futures.event.wait(),
+ timeout=(max(0, end_time - loop.time()) if end_time else None),
+ )
+ # give control back to the caller
+ yield
+ # cancel waiter task
+ for fut in futures:
+ fut.cancel()
+ # panic on failure or timeout
+ try:
+ _panic_or_proceed(
+ futures.done.union(f for f, t in futures.items() if t is not None),
+ timeout_exc_cls=asyncio.TimeoutError,
+ panic=reraise,
+ )
+ except Exception as exc:
+ if tb := exc.__traceback__:
+ while tb.tb_next is not None and any(
+ tb.tb_frame.f_code.co_filename.endswith(name)
+ for name in EXCLUDED_FRAME_FNAMES
+ ):
+ tb = tb.tb_next
+ exc.__traceback__ = tb
+ raise
+
+ def commit(
+ self,
+ task: PregelExecutableTask,
+ exception: BaseException | None,
+ ) -> None:
+ if isinstance(exception, asyncio.CancelledError):
+ # for cancelled tasks, also save error in task,
+ # so loop can finish super-step
+ task.writes.append((ERROR, exception))
+ self.put_writes()(task.id, task.writes) # type: ignore[misc]
+ elif exception:
+ if isinstance(exception, GraphInterrupt):
+ # save interrupt to checkpointer
+ if exception.args[0]:
+ writes = [(INTERRUPT, exception.args[0])]
+ if resumes := [w for w in task.writes if w[0] == RESUME]:
+ writes.extend(resumes)
+ self.put_writes()(task.id, writes) # type: ignore[misc]
+ elif isinstance(exception, GraphBubbleUp):
+ # exception will be raised in _panic_or_proceed
+ pass
+ else:
+ # save error to checkpointer
+ task.writes.append((ERROR, exception))
+ self.put_writes()(task.id, task.writes) # type: ignore[misc]
+ else:
+ if self.node_finished and (
+ task.config is None or TAG_HIDDEN not in task.config.get("tags", [])
+ ):
+ self.node_finished(task.name)
+ if not task.writes:
+ # add no writes marker
+ task.writes.append((NO_WRITES, None))
+ # save task writes to checkpointer
+ self.put_writes()(task.id, task.writes) # type: ignore[misc]
+
+
+def _should_stop_others(
+ done: set[F],
+) -> bool:
+ """Check if any task failed, if so, cancel all other tasks.
+ GraphInterrupts are not considered failures."""
+ for fut in done:
+ if fut.cancelled():
+ continue
+ elif exc := fut.exception():
+ if not isinstance(exc, GraphBubbleUp) and fut not in SKIP_RERAISE_SET:
+ return True
+
+ return False
+
+
+def _exception(
+ fut: concurrent.futures.Future[Any] | asyncio.Future[Any],
+) -> BaseException | None:
+ """Return the exception from a future, without raising CancelledError."""
+ if fut.cancelled():
+ if isinstance(fut, asyncio.Future):
+ return asyncio.CancelledError()
+ else:
+ return concurrent.futures.CancelledError()
+ else:
+ return fut.exception()
+
+
+def _panic_or_proceed(
+ futs: set[concurrent.futures.Future] | set[asyncio.Future],
+ *,
+ timeout_exc_cls: type[Exception] = TimeoutError,
+ panic: bool = True,
+) -> None:
+ """Cancel remaining tasks if any failed, re-raise exception if panic is True."""
+ done: set[concurrent.futures.Future[Any] | asyncio.Future[Any]] = set()
+ inflight: set[concurrent.futures.Future[Any] | asyncio.Future[Any]] = set()
+ for fut in futs:
+ if fut.cancelled():
+ continue
+ elif fut.done():
+ done.add(fut)
+ else:
+ inflight.add(fut)
+ interrupts: list[GraphInterrupt] = []
+ while done:
+ # if any task failed
+ fut = done.pop()
+ if exc := _exception(fut):
+ # cancel all pending tasks
+ while inflight:
+ inflight.pop().cancel()
+ # raise the exception
+ if panic:
+ if isinstance(exc, GraphInterrupt):
+ # collect interrupts
+ interrupts.append(exc)
+ elif fut not in SKIP_RERAISE_SET:
+ raise exc
+ # raise combined interrupts
+ if interrupts:
+ raise GraphInterrupt(tuple(i for exc in interrupts for i in exc.args[0]))
+ if inflight:
+ # if we got here means we timed out
+ while inflight:
+ # cancel all pending tasks
+ inflight.pop().cancel()
+ # raise timeout error
+ raise timeout_exc_cls("Timed out")
+
+
+def _call(
+ task: weakref.ref[PregelExecutableTask],
+ func: Callable[[Any], Awaitable[Any] | Any],
+ input: Any,
+ *,
+ retry_policy: Sequence[RetryPolicy] | None = None,
+ cache_policy: CachePolicy | None = None,
+ callbacks: Callbacks = None,
+ futures: weakref.ref[FuturesDict],
+ schedule_task: Callable[
+ [PregelExecutableTask, int, Call | None], PregelExecutableTask | None
+ ],
+ submit: weakref.ref[Submit],
+) -> concurrent.futures.Future[Any]:
+ if inspect.iscoroutinefunction(func):
+ raise RuntimeError("In an sync context async tasks cannot be called")
+
+ fut: concurrent.futures.Future | None = None
+ # schedule PUSH tasks, collect futures
+ scratchpad: PregelScratchpad = task().config[CONF][CONFIG_KEY_SCRATCHPAD] # type: ignore[union-attr]
+ # schedule the next task, if the callback returns one
+ if next_task := schedule_task(
+ task(), # type: ignore[arg-type]
+ scratchpad.call_counter(),
+ Call(
+ func,
+ input,
+ retry_policy=retry_policy,
+ cache_policy=cache_policy,
+ callbacks=callbacks,
+ ),
+ ):
+ if fut := next(
+ (
+ f
+ for f, t in list(futures().items()) # type: ignore[union-attr]
+ if t is not None and t == next_task.id
+ ),
+ None,
+ ):
+ # if the parent task was retried,
+ # the next task might already be running
+ pass
+ elif next_task.writes:
+ # if it already ran, return the result
+ fut = concurrent.futures.Future()
+ ret = next((v for c, v in next_task.writes if c == RETURN), MISSING)
+ if ret is not MISSING:
+ fut.set_result(ret)
+ elif exc := next((v for c, v in next_task.writes if c == ERROR), None):
+ fut.set_exception(
+ exc if isinstance(exc, BaseException) else Exception(exc)
+ )
+ else:
+ fut.set_result(None)
+ else:
+ # schedule the next task
+ fut = submit()( # type: ignore[misc]
+ run_with_retry,
+ next_task,
+ retry_policy,
+ configurable={
+ CONFIG_KEY_CALL: partial(
+ _call,
+ weakref.ref(next_task),
+ futures=futures,
+ retry_policy=retry_policy,
+ callbacks=callbacks,
+ schedule_task=schedule_task,
+ submit=submit,
+ ),
+ },
+ __reraise_on_exit__=False,
+ # starting a new task in the next tick ensures
+ # updates from this tick are committed/streamed first
+ __next_tick__=True,
+ )
+ # exceptions for call() tasks are raised into the parent task
+ # so we should not re-raise at the end of the tick
+ SKIP_RERAISE_SET.add(fut)
+ futures()[fut] = next_task # type: ignore[index]
+ fut = cast(asyncio.Future | concurrent.futures.Future, fut)
+ # return a chained future to ensure commit() callback is called
+ # before the returned future is resolved, to ensure stream order etc
+ return chain_future(fut, concurrent.futures.Future())
+
+
+def _acall(
+ task: weakref.ref[PregelExecutableTask],
+ func: Callable[[Any], Awaitable[Any] | Any],
+ input: Any,
+ *,
+ retry_policy: Sequence[RetryPolicy] | None = None,
+ cache_policy: CachePolicy | None = None,
+ callbacks: Callbacks = None,
+ # injected dependencies
+ futures: weakref.ref[FuturesDict],
+ schedule_task: Callable[
+ [PregelExecutableTask, int, Call | None],
+ Awaitable[PregelExecutableTask | None],
+ ],
+ submit: weakref.ref[Submit],
+ loop: asyncio.AbstractEventLoop,
+ stream: bool = False,
+) -> asyncio.Future[Any] | concurrent.futures.Future[Any]:
+ # return a chained future to ensure commit() callback is called
+ # before the returned future is resolved, to ensure stream order etc
+ try:
+ in_async = asyncio.current_task() is not None
+ except RuntimeError:
+ in_async = False
+ # if in async context return an async future, otherwise return a sync future
+ if in_async:
+ fut: asyncio.Future[Any] | concurrent.futures.Future[Any] = asyncio.Future(
+ loop=loop
+ )
+ else:
+ fut = concurrent.futures.Future()
+ # schedule the next task
+ run_coroutine_threadsafe(
+ _acall_impl(
+ fut,
+ task,
+ func,
+ input,
+ retry_policy=retry_policy,
+ cache_policy=cache_policy,
+ callbacks=callbacks,
+ futures=futures,
+ schedule_task=schedule_task,
+ submit=submit,
+ loop=loop,
+ stream=stream,
+ ),
+ loop,
+ lazy=False,
+ )
+ return fut
+
+
+async def _acall_impl(
+ destination: asyncio.Future[Any] | concurrent.futures.Future[Any],
+ task: weakref.ref[PregelExecutableTask],
+ func: Callable[[Any], Awaitable[Any] | Any],
+ input: Any,
+ *,
+ retry_policy: Sequence[RetryPolicy] | None = None,
+ cache_policy: CachePolicy | None = None,
+ callbacks: Callbacks = None,
+ # injected dependencies
+ futures: weakref.ref[FuturesDict[asyncio.Future, asyncio.Event]],
+ schedule_task: Callable[
+ [PregelExecutableTask, int, Call | None],
+ Awaitable[PregelExecutableTask | None],
+ ],
+ submit: weakref.ref[Submit],
+ loop: asyncio.AbstractEventLoop,
+ stream: bool = False,
+) -> None:
+ try:
+ fut: asyncio.Future | None = None
+ # schedule PUSH tasks, collect futures
+ scratchpad: PregelScratchpad = task().config[CONF][CONFIG_KEY_SCRATCHPAD] # type: ignore[union-attr]
+ # schedule the next task, if the callback returns one
+ if next_task := await schedule_task(
+ task(), # type: ignore[arg-type]
+ scratchpad.call_counter(),
+ Call(
+ func,
+ input,
+ retry_policy=retry_policy,
+ cache_policy=cache_policy,
+ callbacks=callbacks,
+ ),
+ ):
+ if fut := next(
+ (
+ f
+ for f, t in list(futures().items()) # type: ignore[union-attr]
+ if t is not None and t == next_task.id
+ ),
+ None,
+ ):
+ # if the parent task was retried,
+ # the next task might already be running
+ pass
+ elif next_task.writes:
+ # if it already ran, return the result
+ fut = asyncio.Future(loop=loop)
+ ret = next((v for c, v in next_task.writes if c == RETURN), MISSING)
+ if ret is not MISSING:
+ fut.set_result(ret)
+ elif exc := next((v for c, v in next_task.writes if c == ERROR), None):
+ fut.set_exception(
+ exc if isinstance(exc, BaseException) else Exception(exc)
+ )
+ else:
+ fut.set_result(None)
+ futures()[fut] = next_task # type: ignore[index]
+ else:
+ # schedule the next task
+ fut = cast(
+ asyncio.Future,
+ submit()( # type: ignore[misc]
+ arun_with_retry,
+ next_task,
+ retry_policy,
+ stream=stream,
+ configurable={
+ CONFIG_KEY_CALL: partial(
+ _acall,
+ weakref.ref(next_task),
+ stream=stream,
+ futures=futures,
+ schedule_task=schedule_task,
+ submit=submit,
+ loop=loop,
+ ),
+ },
+ __name__=next_task.name,
+ __cancel_on_exit__=True,
+ __reraise_on_exit__=False,
+ # starting a new task in the next tick ensures
+ # updates from this tick are committed/streamed first
+ __next_tick__=True,
+ ),
+ )
+ # exceptions for call() tasks are raised into the parent task
+ # so we should not re-raise at the end of the tick
+ SKIP_RERAISE_SET.add(fut)
+ futures()[fut] = next_task # type: ignore[index]
+ if fut is not None:
+ chain_future(fut, destination)
+ else:
+ destination.set_exception(RuntimeError("Task not scheduled"))
+ except Exception as exc:
+ destination.set_exception(exc)
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_utils.py b/langgraph/source/libs/langgraph/langgraph/pregel/_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..0c8a14eecac6d125dedf6b77f199cc4b80656f7b
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/_utils.py
@@ -0,0 +1,218 @@
+from __future__ import annotations
+
+import ast
+import inspect
+import re
+import textwrap
+from collections.abc import Callable
+from typing import Any
+
+from langchain_core.runnables import Runnable, RunnableLambda, RunnableSequence
+from langgraph.checkpoint.base import ChannelVersions
+from typing_extensions import override
+
+from langgraph._internal._runnable import RunnableCallable, RunnableSeq
+from langgraph.pregel.protocol import PregelProtocol
+
+
+def get_new_channel_versions(
+ previous_versions: ChannelVersions, current_versions: ChannelVersions
+) -> ChannelVersions:
+ """Get subset of current_versions that are newer than previous_versions."""
+ if previous_versions:
+ version_type = type(next(iter(current_versions.values()), None))
+ null_version = version_type() # type: ignore[misc]
+ new_versions = {
+ k: v
+ for k, v in current_versions.items()
+ if v > previous_versions.get(k, null_version) # type: ignore[operator]
+ }
+ else:
+ new_versions = current_versions
+
+ return new_versions
+
+
+def find_subgraph_pregel(candidate: Runnable) -> PregelProtocol | None:
+ from langgraph.pregel import Pregel
+
+ candidates: list[Runnable] = [candidate]
+
+ for c in candidates:
+ if (
+ isinstance(c, PregelProtocol)
+ # subgraphs that disabled checkpointing are not considered
+ and (not isinstance(c, Pregel) or c.checkpointer is not False)
+ ):
+ return c
+ elif isinstance(c, RunnableSequence) or isinstance(c, RunnableSeq):
+ candidates.extend(c.steps)
+ elif isinstance(c, RunnableLambda):
+ candidates.extend(c.deps)
+ elif isinstance(c, RunnableCallable):
+ if c.func is not None:
+ candidates.extend(
+ nl.__self__ if hasattr(nl, "__self__") else nl
+ for nl in get_function_nonlocals(c.func)
+ )
+ elif c.afunc is not None:
+ candidates.extend(
+ nl.__self__ if hasattr(nl, "__self__") else nl
+ for nl in get_function_nonlocals(c.afunc)
+ )
+
+ return None
+
+
+def get_function_nonlocals(func: Callable) -> list[Any]:
+ """Get the nonlocal variables accessed by a function.
+
+ Args:
+ func: The function to check.
+
+ Returns:
+ List[Any]: The nonlocal variables accessed by the function.
+ """
+ try:
+ code = inspect.getsource(func)
+ tree = ast.parse(textwrap.dedent(code))
+ visitor = FunctionNonLocals()
+ visitor.visit(tree)
+ values: list[Any] = []
+ closure = (
+ inspect.getclosurevars(func.__wrapped__)
+ if hasattr(func, "__wrapped__") and callable(func.__wrapped__)
+ else inspect.getclosurevars(func)
+ )
+ candidates = {**closure.globals, **closure.nonlocals}
+ for k, v in candidates.items():
+ if k in visitor.nonlocals:
+ values.append(v)
+ for kk in visitor.nonlocals:
+ if "." in kk and kk.startswith(k):
+ vv = v
+ for part in kk.split(".")[1:]:
+ if vv is None:
+ break
+ else:
+ try:
+ vv = getattr(vv, part)
+ except AttributeError:
+ break
+ else:
+ values.append(vv)
+ except (SyntaxError, TypeError, OSError, SystemError):
+ return []
+
+ return values
+
+
+class FunctionNonLocals(ast.NodeVisitor):
+ """Get the nonlocal variables accessed of a function."""
+
+ def __init__(self) -> None:
+ self.nonlocals: set[str] = set()
+
+ @override
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> Any:
+ """Visit a function definition.
+
+ Args:
+ node: The node to visit.
+
+ Returns:
+ Any: The result of the visit.
+ """
+ visitor = NonLocals()
+ visitor.visit(node)
+ self.nonlocals.update(visitor.loads - visitor.stores)
+
+ @override
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> Any:
+ """Visit an async function definition.
+
+ Args:
+ node: The node to visit.
+
+ Returns:
+ Any: The result of the visit.
+ """
+ visitor = NonLocals()
+ visitor.visit(node)
+ self.nonlocals.update(visitor.loads - visitor.stores)
+
+ @override
+ def visit_Lambda(self, node: ast.Lambda) -> Any:
+ """Visit a lambda function.
+
+ Args:
+ node: The node to visit.
+
+ Returns:
+ Any: The result of the visit.
+ """
+ visitor = NonLocals()
+ visitor.visit(node)
+ self.nonlocals.update(visitor.loads - visitor.stores)
+
+
+class NonLocals(ast.NodeVisitor):
+ """Get nonlocal variables accessed."""
+
+ def __init__(self) -> None:
+ self.loads: set[str] = set()
+ self.stores: set[str] = set()
+
+ @override
+ def visit_Name(self, node: ast.Name) -> Any:
+ """Visit a name node.
+
+ Args:
+ node: The node to visit.
+
+ Returns:
+ Any: The result of the visit.
+ """
+ if isinstance(node.ctx, ast.Load):
+ self.loads.add(node.id)
+ elif isinstance(node.ctx, ast.Store):
+ self.stores.add(node.id)
+
+ @override
+ def visit_Attribute(self, node: ast.Attribute) -> Any:
+ """Visit an attribute node.
+
+ Args:
+ node: The node to visit.
+
+ Returns:
+ Any: The result of the visit.
+ """
+ if isinstance(node.ctx, ast.Load):
+ parent = node.value
+ attr_expr = node.attr
+ while isinstance(parent, ast.Attribute):
+ attr_expr = parent.attr + "." + attr_expr
+ parent = parent.value
+ if isinstance(parent, ast.Name):
+ self.loads.add(parent.id + "." + attr_expr)
+ self.loads.discard(parent.id)
+ elif isinstance(parent, ast.Call):
+ if isinstance(parent.func, ast.Name):
+ self.loads.add(parent.func.id)
+ else:
+ parent = parent.func
+ attr_expr = ""
+ while isinstance(parent, ast.Attribute):
+ if attr_expr:
+ attr_expr = parent.attr + "." + attr_expr
+ else:
+ attr_expr = parent.attr
+ parent = parent.value
+ if isinstance(parent, ast.Name):
+ self.loads.add(parent.id + "." + attr_expr)
+
+
+def is_xxh3_128_hexdigest(value: str) -> bool:
+ """Check if the given string matches the format of xxh3_128_hexdigest."""
+ return bool(re.fullmatch(r"[0-9a-f]{32}", value))
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_validate.py b/langgraph/source/libs/langgraph/langgraph/pregel/_validate.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcfb54c9a1241699490dcb562020bc9ee83c50d8
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/_validate.py
@@ -0,0 +1,120 @@
+from __future__ import annotations
+
+from collections.abc import Mapping, Sequence
+from typing import Any
+
+from langgraph._internal._constants import RESERVED
+from langgraph.channels.base import BaseChannel
+from langgraph.managed.base import ManagedValueMapping
+from langgraph.pregel._read import PregelNode
+from langgraph.types import All
+
+
+def validate_graph(
+ nodes: Mapping[str, PregelNode],
+ channels: dict[str, BaseChannel],
+ managed: ManagedValueMapping,
+ input_channels: str | Sequence[str],
+ output_channels: str | Sequence[str],
+ stream_channels: str | Sequence[str] | None,
+ interrupt_after_nodes: All | Sequence[str],
+ interrupt_before_nodes: All | Sequence[str],
+) -> None:
+ for chan in channels:
+ if chan in RESERVED:
+ raise ValueError(f"Channel name '{chan}' is reserved")
+ for name in managed:
+ if name in RESERVED:
+ raise ValueError(f"Managed name '{name}' is reserved")
+
+ subscribed_channels = set[str]()
+ for name, node in nodes.items():
+ if name in RESERVED:
+ raise ValueError(f"Node name '{name}' is reserved")
+ if isinstance(node, PregelNode):
+ subscribed_channels.update(node.triggers)
+ if isinstance(node.channels, str):
+ if node.channels not in channels:
+ raise ValueError(
+ f"Node {name} reads channel '{node.channels}' "
+ f"not in known channels: '{repr(sorted(channels))[:100]}'"
+ )
+ else:
+ for chan in node.channels:
+ if chan not in channels and chan not in managed:
+ raise ValueError(
+ f"Node {name} reads channel '{chan}' "
+ f"not in known channels: '{repr(sorted(channels))[:100]}'"
+ )
+ else:
+ raise TypeError(
+ f"Invalid node type {type(node)}, expected PregelNode or NodeBuilder"
+ )
+
+ for chan in subscribed_channels:
+ if chan not in channels:
+ raise ValueError(
+ f"Subscribed channel '{chan}' not "
+ f"in known channels: '{repr(sorted(channels))[:100]}'"
+ )
+
+ if isinstance(input_channels, str):
+ if input_channels not in channels:
+ raise ValueError(
+ f"Input channel '{input_channels}' not "
+ f"in known channels: '{repr(sorted(channels))[:100]}'"
+ )
+ if input_channels not in subscribed_channels:
+ raise ValueError(
+ f"Input channel {input_channels} is not subscribed to by any node"
+ )
+ else:
+ for chan in input_channels:
+ if chan not in channels:
+ raise ValueError(
+ f"Input channel '{chan}' not in '{repr(sorted(channels))[:100]}'"
+ )
+ if all(chan not in subscribed_channels for chan in input_channels):
+ raise ValueError(
+ f"None of the input channels {input_channels} "
+ f"are subscribed to by any node"
+ )
+
+ all_output_channels = set[str]()
+ if isinstance(output_channels, str):
+ all_output_channels.add(output_channels)
+ else:
+ all_output_channels.update(output_channels)
+ if isinstance(stream_channels, str):
+ all_output_channels.add(stream_channels)
+ elif stream_channels is not None:
+ all_output_channels.update(stream_channels)
+
+ for chan in all_output_channels:
+ if chan not in channels:
+ raise ValueError(
+ f"Output channel '{chan}' not "
+ f"in known channels: '{repr(sorted(channels))[:100]}'"
+ )
+
+ if interrupt_after_nodes != "*":
+ for n in interrupt_after_nodes:
+ if n not in nodes:
+ raise ValueError(f"Node {n} not in nodes")
+ if interrupt_before_nodes != "*":
+ for n in interrupt_before_nodes:
+ if n not in nodes:
+ raise ValueError(f"Node {n} not in nodes")
+
+
+def validate_keys(
+ keys: str | Sequence[str] | None,
+ channels: Mapping[str, Any],
+) -> None:
+ if isinstance(keys, str):
+ if keys not in channels:
+ raise ValueError(f"Key {keys} not in channels")
+ elif keys is not None:
+ for chan in keys:
+ if chan not in channels:
+ raise ValueError(f"Key {chan} not in channels")
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/_write.py b/langgraph/source/libs/langgraph/langgraph/pregel/_write.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b4508257653e54c94cb2360bd32de91bb161bfd
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/_write.py
@@ -0,0 +1,192 @@
+from __future__ import annotations
+
+from collections.abc import Callable, Sequence
+from typing import (
+ Any,
+ NamedTuple,
+ TypeVar,
+ cast,
+)
+
+from langchain_core.runnables import Runnable, RunnableConfig
+
+from langgraph._internal._constants import CONF, CONFIG_KEY_SEND, TASKS
+from langgraph._internal._runnable import RunnableCallable
+from langgraph._internal._typing import MISSING
+from langgraph.errors import InvalidUpdateError
+from langgraph.types import Send
+
+TYPE_SEND = Callable[[Sequence[tuple[str, Any]]], None]
+R = TypeVar("R", bound=Runnable)
+
+SKIP_WRITE = object()
+PASSTHROUGH = object()
+
+
+class ChannelWriteEntry(NamedTuple):
+ channel: str
+ """Channel name to write to."""
+ value: Any = PASSTHROUGH
+ """Value to write, or PASSTHROUGH to use the input."""
+ skip_none: bool = False
+ """Whether to skip writing if the value is None."""
+ mapper: Callable | None = None
+ """Function to transform the value before writing."""
+
+
+class ChannelWriteTupleEntry(NamedTuple):
+ mapper: Callable[[Any], Sequence[tuple[str, Any]] | None]
+ """Function to extract tuples from value."""
+ value: Any = PASSTHROUGH
+ """Value to write, or PASSTHROUGH to use the input."""
+ static: Sequence[tuple[str, Any, str | None]] | None = None
+ """Optional, declared writes for static analysis."""
+
+
+class ChannelWrite(RunnableCallable):
+ """Implements the logic for sending writes to CONFIG_KEY_SEND.
+ Can be used as a runnable or as a static method to call imperatively."""
+
+ writes: list[ChannelWriteEntry | ChannelWriteTupleEntry | Send]
+ """Sequence of write entries or Send objects to write."""
+
+ def __init__(
+ self,
+ writes: Sequence[ChannelWriteEntry | ChannelWriteTupleEntry | Send],
+ *,
+ tags: Sequence[str] | None = None,
+ ):
+ super().__init__(
+ func=self._write,
+ afunc=self._awrite,
+ name=None,
+ tags=tags,
+ trace=False,
+ )
+ self.writes = cast(
+ list[ChannelWriteEntry | ChannelWriteTupleEntry | Send], writes
+ )
+
+ def get_name(self, suffix: str | None = None, *, name: str | None = None) -> str:
+ if not name:
+ name = f"ChannelWrite<{','.join(w.channel if isinstance(w, ChannelWriteEntry) else '...' if isinstance(w, ChannelWriteTupleEntry) else w.node for w in self.writes)}>"
+ return super().get_name(suffix, name=name)
+
+ def _write(self, input: Any, config: RunnableConfig) -> None:
+ writes = [
+ ChannelWriteEntry(write.channel, input, write.skip_none, write.mapper)
+ if isinstance(write, ChannelWriteEntry) and write.value is PASSTHROUGH
+ else ChannelWriteTupleEntry(write.mapper, input)
+ if isinstance(write, ChannelWriteTupleEntry) and write.value is PASSTHROUGH
+ else write
+ for write in self.writes
+ ]
+ self.do_write(
+ config,
+ writes,
+ )
+ return input
+
+ async def _awrite(self, input: Any, config: RunnableConfig) -> None:
+ writes = [
+ ChannelWriteEntry(write.channel, input, write.skip_none, write.mapper)
+ if isinstance(write, ChannelWriteEntry) and write.value is PASSTHROUGH
+ else ChannelWriteTupleEntry(write.mapper, input)
+ if isinstance(write, ChannelWriteTupleEntry) and write.value is PASSTHROUGH
+ else write
+ for write in self.writes
+ ]
+ self.do_write(
+ config,
+ writes,
+ )
+ return input
+
+ @staticmethod
+ def do_write(
+ config: RunnableConfig,
+ writes: Sequence[ChannelWriteEntry | ChannelWriteTupleEntry | Send],
+ allow_passthrough: bool = True,
+ ) -> None:
+ # validate
+ for w in writes:
+ if isinstance(w, ChannelWriteEntry):
+ if w.channel == TASKS:
+ raise InvalidUpdateError(
+ "Cannot write to the reserved channel TASKS"
+ )
+ if w.value is PASSTHROUGH and not allow_passthrough:
+ raise InvalidUpdateError("PASSTHROUGH value must be replaced")
+ if isinstance(w, ChannelWriteTupleEntry):
+ if w.value is PASSTHROUGH and not allow_passthrough:
+ raise InvalidUpdateError("PASSTHROUGH value must be replaced")
+ # if we want to persist writes found before hitting a ParentCommand
+ # can move this to a finally block
+ write: TYPE_SEND = config[CONF][CONFIG_KEY_SEND]
+ write(_assemble_writes(writes))
+
+ @staticmethod
+ def is_writer(runnable: Runnable) -> bool:
+ """Used by PregelNode to distinguish between writers and other runnables."""
+ return (
+ isinstance(runnable, ChannelWrite)
+ or getattr(runnable, "_is_channel_writer", MISSING) is not MISSING
+ )
+
+ @staticmethod
+ def get_static_writes(
+ runnable: Runnable,
+ ) -> Sequence[tuple[str, Any, str | None]] | None:
+ """Used to get conditional writes a writer declares for static analysis."""
+ if isinstance(runnable, ChannelWrite):
+ return [
+ w
+ for entry in runnable.writes
+ if isinstance(entry, ChannelWriteTupleEntry) and entry.static
+ for w in entry.static
+ ] or None
+ elif writes := getattr(runnable, "_is_channel_writer", MISSING):
+ if writes is not MISSING:
+ writes = cast(
+ Sequence[tuple[ChannelWriteEntry | Send, str | None]],
+ writes,
+ )
+ entries = [e for e, _ in writes]
+ labels = [la for _, la in writes]
+ return [(*t, la) for t, la in zip(_assemble_writes(entries), labels)]
+
+ @staticmethod
+ def register_writer(
+ runnable: R,
+ static: Sequence[tuple[ChannelWriteEntry | Send, str | None]] | None = None,
+ ) -> R:
+ """Used to mark a runnable as a writer, so that it can be detected by is_writer.
+ Instances of ChannelWrite are automatically marked as writers.
+ Optionally, a list of declared writes can be passed for static analysis."""
+ # using object.__setattr__ to work around objects that override __setattr__
+ # eg. pydantic models and dataclasses
+ object.__setattr__(runnable, "_is_channel_writer", static)
+ return runnable
+
+
+def _assemble_writes(
+ writes: Sequence[ChannelWriteEntry | ChannelWriteTupleEntry | Send],
+) -> list[tuple[str, Any]]:
+ """Assembles the writes into a list of tuples."""
+ tuples: list[tuple[str, Any]] = []
+ for w in writes:
+ if isinstance(w, Send):
+ tuples.append((TASKS, w))
+ elif isinstance(w, ChannelWriteTupleEntry):
+ if ww := w.mapper(w.value):
+ tuples.extend(ww)
+ elif isinstance(w, ChannelWriteEntry):
+ value = w.mapper(w.value) if w.mapper is not None else w.value
+ if value is SKIP_WRITE:
+ continue
+ if w.skip_none and value is None:
+ continue
+ tuples.append((w.channel, value))
+ else:
+ raise ValueError(f"Invalid write entry: {w}")
+ return tuples
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/debug.py b/langgraph/source/libs/langgraph/langgraph/pregel/debug.py
new file mode 100644
index 0000000000000000000000000000000000000000..0de53e12024b5d4dfc17b1fc770e1367df40fb94
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/debug.py
@@ -0,0 +1,308 @@
+from __future__ import annotations
+
+from collections.abc import Iterable, Iterator, Mapping, Sequence
+from dataclasses import asdict
+from typing import Any
+from uuid import UUID
+
+from langchain_core.runnables import RunnableConfig
+from langgraph.checkpoint.base import CheckpointMetadata, PendingWrite
+from typing_extensions import TypedDict
+
+from langgraph._internal._config import patch_checkpoint_map
+from langgraph._internal._constants import (
+ CONF,
+ CONFIG_KEY_CHECKPOINT_NS,
+ ERROR,
+ INTERRUPT,
+ NS_END,
+ NS_SEP,
+ RETURN,
+)
+from langgraph._internal._typing import MISSING
+from langgraph.channels.base import BaseChannel
+from langgraph.constants import TAG_HIDDEN
+from langgraph.pregel._io import read_channels
+from langgraph.types import PregelExecutableTask, PregelTask, StateSnapshot
+
+__all__ = ("TaskPayload", "TaskResultPayload", "CheckpointTask", "CheckpointPayload")
+
+
+class TaskPayload(TypedDict):
+ id: str
+ name: str
+ input: Any
+ triggers: list[str]
+
+
+class TaskResultPayload(TypedDict):
+ id: str
+ name: str
+ error: str | None
+ interrupts: list[dict]
+ result: dict[str, Any]
+
+
+class CheckpointTask(TypedDict):
+ id: str
+ name: str
+ error: str | None
+ interrupts: list[dict]
+ state: StateSnapshot | RunnableConfig | None
+
+
+class CheckpointPayload(TypedDict):
+ config: RunnableConfig | None
+ metadata: CheckpointMetadata
+ values: dict[str, Any]
+ next: list[str]
+ parent_config: RunnableConfig | None
+ tasks: list[CheckpointTask]
+
+
+TASK_NAMESPACE = UUID("6ba7b831-9dad-11d1-80b4-00c04fd430c8")
+
+
+def map_debug_tasks(tasks: Iterable[PregelExecutableTask]) -> Iterator[TaskPayload]:
+ """Produce "task" events for stream_mode=debug."""
+ for task in tasks:
+ if task.config is not None and TAG_HIDDEN in task.config.get("tags", []):
+ continue
+
+ yield {
+ "id": task.id,
+ "name": task.name,
+ "input": task.input,
+ "triggers": task.triggers,
+ }
+
+
+def is_multiple_channel_write(value: Any) -> bool:
+ """Return True if the payload already wraps multiple writes from the same channel."""
+ return (
+ isinstance(value, dict)
+ and "$writes" in value
+ and isinstance(value["$writes"], list)
+ )
+
+
+def map_task_result_writes(writes: Sequence[tuple[str, Any]]) -> dict[str, Any]:
+ """Folds task writes into a result dict and aggregates multiple writes to the same channel.
+
+ If the channel contains a single write, we record the write in the result dict as `{channel: write}`
+ If the channel contains multiple writes, we record the writes in the result dict as `{channel: {'$writes': [write1, write2, ...]}}`"""
+
+ result: dict[str, Any] = {}
+ for channel, value in writes:
+ existing = result.get(channel)
+
+ if existing is not None:
+ channel_writes = (
+ existing["$writes"]
+ if is_multiple_channel_write(existing)
+ else [existing]
+ )
+ channel_writes.append(value)
+ result[channel] = {"$writes": channel_writes}
+ else:
+ result[channel] = value
+ return result
+
+
+def map_debug_task_results(
+ task_tup: tuple[PregelExecutableTask, Sequence[tuple[str, Any]]],
+ stream_keys: str | Sequence[str],
+) -> Iterator[TaskResultPayload]:
+ """Produce "task_result" events for stream_mode=debug."""
+ stream_channels_list = (
+ [stream_keys] if isinstance(stream_keys, str) else stream_keys
+ )
+ task, writes = task_tup
+ yield {
+ "id": task.id,
+ "name": task.name,
+ "error": next((w[1] for w in writes if w[0] == ERROR), None),
+ "result": map_task_result_writes(
+ [w for w in writes if w[0] in stream_channels_list or w[0] == RETURN]
+ ),
+ "interrupts": [
+ asdict(v)
+ for w in writes
+ if w[0] == INTERRUPT
+ for v in (w[1] if isinstance(w[1], Sequence) else [w[1]])
+ ],
+ }
+
+
+def rm_pregel_keys(config: RunnableConfig | None) -> RunnableConfig | None:
+ """Remove pregel-specific keys from the config."""
+ if config is None:
+ return config
+ return {
+ "configurable": {
+ k: v
+ for k, v in config.get("configurable", {}).items()
+ if not k.startswith("__pregel_")
+ }
+ }
+
+
+def map_debug_checkpoint(
+ config: RunnableConfig,
+ channels: Mapping[str, BaseChannel],
+ stream_channels: str | Sequence[str],
+ metadata: CheckpointMetadata,
+ tasks: Iterable[PregelExecutableTask],
+ pending_writes: list[PendingWrite],
+ parent_config: RunnableConfig | None,
+ output_keys: str | Sequence[str],
+) -> Iterator[CheckpointPayload]:
+ """Produce "checkpoint" events for stream_mode=debug."""
+
+ parent_ns = config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")
+ task_states: dict[str, RunnableConfig | StateSnapshot] = {}
+
+ for task in tasks:
+ if not task.subgraphs:
+ continue
+
+ # assemble checkpoint_ns for this task
+ task_ns = f"{task.name}{NS_END}{task.id}"
+ if parent_ns:
+ task_ns = f"{parent_ns}{NS_SEP}{task_ns}"
+
+ # set config as signal that subgraph checkpoints exist
+ task_states[task.id] = {
+ CONF: {
+ "thread_id": config[CONF]["thread_id"],
+ CONFIG_KEY_CHECKPOINT_NS: task_ns,
+ }
+ }
+
+ yield {
+ "config": rm_pregel_keys(patch_checkpoint_map(config, metadata)),
+ "parent_config": rm_pregel_keys(patch_checkpoint_map(parent_config, metadata)),
+ "values": read_channels(channels, stream_channels),
+ "metadata": metadata,
+ "next": [t.name for t in tasks],
+ "tasks": [
+ {
+ "id": t.id,
+ "name": t.name,
+ "error": t.error,
+ "state": t.state,
+ }
+ if t.error
+ else {
+ "id": t.id,
+ "name": t.name,
+ "result": t.result,
+ "interrupts": tuple(asdict(i) for i in t.interrupts),
+ "state": t.state,
+ }
+ if t.result
+ else {
+ "id": t.id,
+ "name": t.name,
+ "interrupts": tuple(asdict(i) for i in t.interrupts),
+ "state": t.state,
+ }
+ for t in tasks_w_writes(tasks, pending_writes, task_states, output_keys)
+ ],
+ }
+
+
+def tasks_w_writes(
+ tasks: Iterable[PregelTask | PregelExecutableTask],
+ pending_writes: list[PendingWrite] | None,
+ states: dict[str, RunnableConfig | StateSnapshot] | None,
+ output_keys: str | Sequence[str],
+) -> tuple[PregelTask, ...]:
+ """Apply writes / subgraph states to tasks to be returned in a StateSnapshot."""
+ pending_writes = pending_writes or []
+ out: list[PregelTask] = []
+ for task in tasks:
+ rtn = next(
+ (
+ val
+ for tid, chan, val in pending_writes
+ if tid == task.id and chan == RETURN
+ ),
+ MISSING,
+ )
+ task_error = next(
+ (exc for tid, n, exc in pending_writes if tid == task.id and n == ERROR),
+ None,
+ )
+ task_interrupts = tuple(
+ v
+ for tid, n, vv in pending_writes
+ if tid == task.id and n == INTERRUPT
+ for v in (vv if isinstance(vv, Sequence) else [vv])
+ )
+
+ task_writes = [
+ (chan, val)
+ for tid, chan, val in pending_writes
+ if tid == task.id and chan not in (ERROR, INTERRUPT, RETURN)
+ ]
+
+ if rtn is not MISSING:
+ task_result = rtn
+ elif isinstance(output_keys, str):
+ # unwrap single channel writes to just the write value
+ filtered_writes = [
+ (chan, val) for chan, val in task_writes if chan == output_keys
+ ]
+ mapped_writes = map_task_result_writes(filtered_writes)
+ task_result = mapped_writes.get(str(output_keys)) if mapped_writes else None
+ else:
+ if isinstance(output_keys, str):
+ output_keys = [output_keys]
+ # map task result writes to the desired output channels
+ # repeateed writes to the same channel are aggregated into: {'$writes': [write1, write2, ...]}
+ filtered_writes = [
+ (chan, val) for chan, val in task_writes if chan in output_keys
+ ]
+ mapped_writes = map_task_result_writes(filtered_writes)
+ task_result = mapped_writes if filtered_writes else {}
+
+ has_writes = rtn is not MISSING or any(
+ w[0] == task.id and w[1] not in (ERROR, INTERRUPT) for w in pending_writes
+ )
+
+ out.append(
+ PregelTask(
+ task.id,
+ task.name,
+ task.path,
+ task_error,
+ task_interrupts,
+ states.get(task.id) if states else None,
+ task_result if has_writes else None,
+ )
+ )
+ return tuple(out)
+
+
+COLOR_MAPPING = {
+ "black": "0;30",
+ "red": "0;31",
+ "green": "0;32",
+ "yellow": "0;33",
+ "blue": "0;34",
+ "magenta": "0;35",
+ "cyan": "0;36",
+ "white": "0;37",
+ "gray": "1;30",
+}
+
+
+def get_colored_text(text: str, color: str) -> str:
+ """Get colored text."""
+ return f"\033[1;3{COLOR_MAPPING[color]}m{text}\033[0m"
+
+
+def get_bolded_text(text: str) -> str:
+ """Get bolded text."""
+ return f"\033[1m{text}\033[0m"
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/main.py b/langgraph/source/libs/langgraph/langgraph/pregel/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..37e8125f9e4580eaadf36649c79e16b64078c196
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/main.py
@@ -0,0 +1,3322 @@
+from __future__ import annotations
+
+import asyncio
+import concurrent
+import concurrent.futures
+import contextlib
+import queue
+import warnings
+import weakref
+from collections import defaultdict, deque
+from collections.abc import (
+ AsyncIterator,
+ Awaitable,
+ Callable,
+ Iterator,
+ Mapping,
+ Sequence,
+)
+from dataclasses import is_dataclass
+from functools import partial
+from inspect import isclass
+from typing import (
+ Any,
+ Generic,
+ cast,
+ get_type_hints,
+)
+from uuid import UUID, uuid5
+
+from langchain_core.globals import get_debug
+from langchain_core.runnables import (
+ RunnableSequence,
+)
+from langchain_core.runnables.base import Input, Output
+from langchain_core.runnables.config import (
+ RunnableConfig,
+ get_async_callback_manager_for_config,
+ get_callback_manager_for_config,
+)
+from langchain_core.runnables.graph import Graph
+from langgraph.cache.base import BaseCache
+from langgraph.checkpoint.base import (
+ BaseCheckpointSaver,
+ Checkpoint,
+ CheckpointTuple,
+)
+from langgraph.store.base import BaseStore
+from pydantic import BaseModel, TypeAdapter
+from typing_extensions import Self, Unpack, deprecated, is_typeddict
+
+from langgraph._internal._config import (
+ ensure_config,
+ merge_configs,
+ patch_checkpoint_map,
+ patch_config,
+ patch_configurable,
+ recast_checkpoint_ns,
+)
+from langgraph._internal._constants import (
+ CACHE_NS_WRITES,
+ CONF,
+ CONFIG_KEY_CACHE,
+ CONFIG_KEY_CHECKPOINT_ID,
+ CONFIG_KEY_CHECKPOINT_NS,
+ CONFIG_KEY_CHECKPOINTER,
+ CONFIG_KEY_DURABILITY,
+ CONFIG_KEY_NODE_FINISHED,
+ CONFIG_KEY_READ,
+ CONFIG_KEY_RUNNER_SUBMIT,
+ CONFIG_KEY_RUNTIME,
+ CONFIG_KEY_SEND,
+ CONFIG_KEY_STREAM,
+ CONFIG_KEY_TASK_ID,
+ CONFIG_KEY_THREAD_ID,
+ ERROR,
+ INPUT,
+ INTERRUPT,
+ NS_END,
+ NS_SEP,
+ NULL_TASK_ID,
+ PUSH,
+ TASKS,
+)
+from langgraph._internal._pydantic import create_model
+from langgraph._internal._queue import ( # type: ignore[attr-defined]
+ AsyncQueue,
+ SyncQueue,
+)
+from langgraph._internal._runnable import (
+ Runnable,
+ RunnableLike,
+ RunnableSeq,
+ coerce_to_runnable,
+)
+from langgraph._internal._typing import MISSING, DeprecatedKwargs
+from langgraph.channels.base import BaseChannel
+from langgraph.channels.topic import Topic
+from langgraph.config import get_config
+from langgraph.constants import END
+from langgraph.errors import (
+ ErrorCode,
+ GraphRecursionError,
+ InvalidUpdateError,
+ create_error_message,
+)
+from langgraph.managed.base import ManagedValueSpec
+from langgraph.pregel._algo import (
+ PregelTaskWrites,
+ _scratchpad,
+ apply_writes,
+ local_read,
+ prepare_next_tasks,
+)
+from langgraph.pregel._call import identifier
+from langgraph.pregel._checkpoint import (
+ channels_from_checkpoint,
+ copy_checkpoint,
+ create_checkpoint,
+ empty_checkpoint,
+)
+from langgraph.pregel._draw import draw_graph
+from langgraph.pregel._io import map_input, read_channels
+from langgraph.pregel._loop import AsyncPregelLoop, SyncPregelLoop
+from langgraph.pregel._messages import StreamMessagesHandler
+from langgraph.pregel._read import DEFAULT_BOUND, PregelNode
+from langgraph.pregel._retry import RetryPolicy
+from langgraph.pregel._runner import PregelRunner
+from langgraph.pregel._utils import get_new_channel_versions
+from langgraph.pregel._validate import validate_graph, validate_keys
+from langgraph.pregel._write import ChannelWrite, ChannelWriteEntry
+from langgraph.pregel.debug import get_bolded_text, get_colored_text, tasks_w_writes
+from langgraph.pregel.protocol import PregelProtocol, StreamChunk, StreamProtocol
+from langgraph.runtime import DEFAULT_RUNTIME, Runtime
+from langgraph.types import (
+ All,
+ CachePolicy,
+ Checkpointer,
+ Command,
+ Durability,
+ Interrupt,
+ Send,
+ StateSnapshot,
+ StateUpdate,
+ StreamMode,
+ ensure_valid_checkpointer,
+)
+from langgraph.typing import ContextT, InputT, OutputT, StateT
+from langgraph.warnings import LangGraphDeprecatedSinceV10
+
+try:
+ from langchain_core.tracers._streaming import _StreamingCallbackHandler
+except ImportError:
+ _StreamingCallbackHandler = None # type: ignore
+
+__all__ = ("NodeBuilder", "Pregel")
+
+_WriteValue = Callable[[Input], Output] | Any
+
+
+class NodeBuilder:
+ __slots__ = (
+ "_channels",
+ "_triggers",
+ "_tags",
+ "_metadata",
+ "_writes",
+ "_bound",
+ "_retry_policy",
+ "_cache_policy",
+ )
+
+ _channels: str | list[str]
+ _triggers: list[str]
+ _tags: list[str]
+ _metadata: dict[str, Any]
+ _writes: list[ChannelWriteEntry]
+ _bound: Runnable
+ _retry_policy: list[RetryPolicy]
+ _cache_policy: CachePolicy | None
+
+ def __init__(
+ self,
+ ) -> None:
+ self._channels = []
+ self._triggers = []
+ self._tags = []
+ self._metadata = {}
+ self._writes = []
+ self._bound = DEFAULT_BOUND
+ self._retry_policy = []
+ self._cache_policy = None
+
+ def subscribe_only(
+ self,
+ channel: str,
+ ) -> Self:
+ """Subscribe to a single channel."""
+ if not self._channels:
+ self._channels = channel
+ else:
+ raise ValueError(
+ "Cannot subscribe to single channels when other channels are already subscribed to"
+ )
+
+ self._triggers.append(channel)
+
+ return self
+
+ def subscribe_to(
+ self,
+ *channels: str,
+ read: bool = True,
+ ) -> Self:
+ """Add channels to subscribe to.
+
+ Node will be invoked when any of these channels are updated, with a dict of the
+ channel values as input.
+
+ Args:
+ channels: Channel name(s) to subscribe to
+ read: If `True`, the channels will be included in the input to the node.
+ Otherwise, they will trigger the node without being sent in input.
+
+ Returns:
+ Self for chaining
+ """
+ if isinstance(self._channels, str):
+ raise ValueError(
+ "Cannot subscribe to channels when subscribed to a single channel"
+ )
+ if read:
+ if not self._channels:
+ self._channels = list(channels)
+ else:
+ self._channels.extend(channels)
+
+ if isinstance(channels, str):
+ self._triggers.append(channels)
+ else:
+ self._triggers.extend(channels)
+
+ return self
+
+ def read_from(
+ self,
+ *channels: str,
+ ) -> Self:
+ """Adds the specified channels to read from, without subscribing to them."""
+ assert isinstance(self._channels, list), (
+ "Cannot read additional channels when subscribed to single channels"
+ )
+ self._channels.extend(channels)
+ return self
+
+ def do(
+ self,
+ node: RunnableLike,
+ ) -> Self:
+ """Adds the specified node."""
+ if self._bound is not DEFAULT_BOUND:
+ self._bound = RunnableSeq(
+ self._bound, coerce_to_runnable(node, name=None, trace=True)
+ )
+ else:
+ self._bound = coerce_to_runnable(node, name=None, trace=True)
+ return self
+
+ def write_to(
+ self,
+ *channels: str | ChannelWriteEntry,
+ **kwargs: _WriteValue,
+ ) -> Self:
+ """Add channel writes.
+
+ Args:
+ *channels: Channel names to write to.
+ **kwargs: Channel name and value mappings.
+
+ Returns:
+ Self for chaining
+ """
+ self._writes.extend(
+ ChannelWriteEntry(c) if isinstance(c, str) else c for c in channels
+ )
+ self._writes.extend(
+ ChannelWriteEntry(k, mapper=v)
+ if callable(v)
+ else ChannelWriteEntry(k, value=v)
+ for k, v in kwargs.items()
+ )
+
+ return self
+
+ def meta(self, *tags: str, **metadata: Any) -> Self:
+ """Add tags or metadata to the node."""
+ self._tags.extend(tags)
+ self._metadata.update(metadata)
+ return self
+
+ def add_retry_policies(self, *policies: RetryPolicy) -> Self:
+ """Adds retry policies to the node."""
+ self._retry_policy.extend(policies)
+ return self
+
+ def add_cache_policy(self, policy: CachePolicy) -> Self:
+ """Adds cache policies to the node."""
+ self._cache_policy = policy
+ return self
+
+ def build(self) -> PregelNode:
+ """Builds the node."""
+ return PregelNode(
+ channels=self._channels,
+ triggers=self._triggers,
+ tags=self._tags,
+ metadata=self._metadata,
+ writers=[ChannelWrite(self._writes)],
+ bound=self._bound,
+ retry_policy=self._retry_policy,
+ cache_policy=self._cache_policy,
+ )
+
+
+class Pregel(
+ PregelProtocol[StateT, ContextT, InputT, OutputT],
+ Generic[StateT, ContextT, InputT, OutputT],
+):
+ """Pregel manages the runtime behavior for LangGraph applications.
+
+ ## Overview
+
+ Pregel combines [**actors**](https://en.wikipedia.org/wiki/Actor_model)
+ and **channels** into a single application.
+ **Actors** read data from channels and write data to channels.
+ Pregel organizes the execution of the application into multiple steps,
+ following the **Pregel Algorithm**/**Bulk Synchronous Parallel** model.
+
+ Each step consists of three phases:
+
+ - **Plan**: Determine which **actors** to execute in this step. For example,
+ in the first step, select the **actors** that subscribe to the special
+ **input** channels; in subsequent steps,
+ select the **actors** that subscribe to channels updated in the previous step.
+ - **Execution**: Execute all selected **actors** in parallel,
+ until all complete, or one fails, or a timeout is reached. During this
+ phase, channel updates are invisible to actors until the next step.
+ - **Update**: Update the channels with the values written by the **actors**
+ in this step.
+
+ Repeat until no **actors** are selected for execution, or a maximum number of
+ steps is reached.
+
+ ## Actors
+
+ An **actor** is a `PregelNode`.
+ It subscribes to channels, reads data from them, and writes data to them.
+ It can be thought of as an **actor** in the Pregel algorithm.
+ `PregelNodes` implement LangChain's
+ Runnable interface.
+
+ ## Channels
+
+ Channels are used to communicate between actors (`PregelNodes`).
+ Each channel has a value type, an update type, and an update function – which
+ takes a sequence of updates and
+ modifies the stored value. Channels can be used to send data from one chain to
+ another, or to send data from a chain to itself in a future step. LangGraph
+ provides a number of built-in channels:
+
+ ### Basic channels: LastValue and Topic
+
+ - `LastValue`: The default channel, stores the last value sent to the channel,
+ useful for input and output values, or for sending data from one step to the next
+ - `Topic`: A configurable PubSub Topic, useful for sending multiple values
+ between *actors*, or for accumulating output. Can be configured to deduplicate
+ values, and/or to accumulate values over the course of multiple steps.
+
+ ### Advanced channels: Context and BinaryOperatorAggregate
+
+ - `Context`: exposes the value of a context manager, managing its lifecycle.
+ Useful for accessing external resources that require setup and/or teardown. e.g.
+ `client = Context(httpx.Client)`
+ - `BinaryOperatorAggregate`: stores a persistent value, updated by applying
+ a binary operator to the current value and each update
+ sent to the channel, useful for computing aggregates over multiple steps. e.g.
+ `total = BinaryOperatorAggregate(int, operator.add)`
+
+ ## Examples
+
+ Most users will interact with Pregel via a
+ [StateGraph (Graph API)][langgraph.graph.StateGraph] or via an
+ [entrypoint (Functional API)][langgraph.func.entrypoint].
+
+ However, for **advanced** use cases, Pregel can be used directly. If you're
+ not sure whether you need to use Pregel directly, then the answer is probably no
+ - you should use the Graph API or Functional API instead. These are higher-level
+ interfaces that will compile down to Pregel under the hood.
+
+ Here are some examples to give you a sense of how it works:
+
+ Example: Single node application
+ ```python
+ from langgraph.channels import EphemeralValue
+ from langgraph.pregel import Pregel, NodeBuilder
+
+ node1 = (
+ NodeBuilder().subscribe_only("a")
+ .do(lambda x: x + x)
+ .write_to("b")
+ )
+
+ app = Pregel(
+ nodes={"node1": node1},
+ channels={
+ "a": EphemeralValue(str),
+ "b": EphemeralValue(str),
+ },
+ input_channels=["a"],
+ output_channels=["b"],
+ )
+
+ app.invoke({"a": "foo"})
+ ```
+
+ ```con
+ {'b': 'foofoo'}
+ ```
+
+ Example: Using multiple nodes and multiple output channels
+ ```python
+ from langgraph.channels import LastValue, EphemeralValue
+ from langgraph.pregel import Pregel, NodeBuilder
+
+ node1 = (
+ NodeBuilder().subscribe_only("a")
+ .do(lambda x: x + x)
+ .write_to("b")
+ )
+
+ node2 = (
+ NodeBuilder().subscribe_to("b")
+ .do(lambda x: x["b"] + x["b"])
+ .write_to("c")
+ )
+
+
+ app = Pregel(
+ nodes={"node1": node1, "node2": node2},
+ channels={
+ "a": EphemeralValue(str),
+ "b": LastValue(str),
+ "c": EphemeralValue(str),
+ },
+ input_channels=["a"],
+ output_channels=["b", "c"],
+ )
+
+ app.invoke({"a": "foo"})
+ ```
+
+ ```con
+ {'b': 'foofoo', 'c': 'foofoofoofoo'}
+ ```
+
+ Example: Using a Topic channel
+ ```python
+ from langgraph.channels import LastValue, EphemeralValue, Topic
+ from langgraph.pregel import Pregel, NodeBuilder
+
+ node1 = (
+ NodeBuilder().subscribe_only("a")
+ .do(lambda x: x + x)
+ .write_to("b", "c")
+ )
+
+ node2 = (
+ NodeBuilder().subscribe_only("b")
+ .do(lambda x: x + x)
+ .write_to("c")
+ )
+
+
+ app = Pregel(
+ nodes={"node1": node1, "node2": node2},
+ channels={
+ "a": EphemeralValue(str),
+ "b": EphemeralValue(str),
+ "c": Topic(str, accumulate=True),
+ },
+ input_channels=["a"],
+ output_channels=["c"],
+ )
+
+ app.invoke({"a": "foo"})
+ ```
+
+ ```pycon
+ {"c": ["foofoo", "foofoofoofoo"]}
+ ```
+
+ Example: Using a `BinaryOperatorAggregate` channel
+ ```python
+ from langgraph.channels import EphemeralValue, BinaryOperatorAggregate
+ from langgraph.pregel import Pregel, NodeBuilder
+
+
+ node1 = (
+ NodeBuilder().subscribe_only("a")
+ .do(lambda x: x + x)
+ .write_to("b", "c")
+ )
+
+ node2 = (
+ NodeBuilder().subscribe_only("b")
+ .do(lambda x: x + x)
+ .write_to("c")
+ )
+
+
+ def reducer(current, update):
+ if current:
+ return current + " | " + update
+ else:
+ return update
+
+
+ app = Pregel(
+ nodes={"node1": node1, "node2": node2},
+ channels={
+ "a": EphemeralValue(str),
+ "b": EphemeralValue(str),
+ "c": BinaryOperatorAggregate(str, operator=reducer),
+ },
+ input_channels=["a"],
+ output_channels=["c"],
+ )
+
+ app.invoke({"a": "foo"})
+ ```
+
+ ```con
+ {'c': 'foofoo | foofoofoofoo'}
+ ```
+
+ Example: Introducing a cycle
+ This example demonstrates how to introduce a cycle in the graph, by having
+ a chain write to a channel it subscribes to.
+
+ Execution will continue until a `None` value is written to the channel.
+
+ ```python
+ from langgraph.channels import EphemeralValue
+ from langgraph.pregel import Pregel, NodeBuilder, ChannelWriteEntry
+
+ example_node = (
+ NodeBuilder()
+ .subscribe_only("value")
+ .do(lambda x: x + x if len(x) < 10 else None)
+ .write_to(ChannelWriteEntry(channel="value", skip_none=True))
+ )
+
+ app = Pregel(
+ nodes={"example_node": example_node},
+ channels={
+ "value": EphemeralValue(str),
+ },
+ input_channels=["value"],
+ output_channels=["value"],
+ )
+
+ app.invoke({"value": "a"})
+ ```
+
+ ```con
+ {'value': 'aaaaaaaaaaaaaaaa'}
+ ```
+ """
+
+ nodes: dict[str, PregelNode]
+
+ channels: dict[str, BaseChannel | ManagedValueSpec]
+
+ stream_mode: StreamMode = "values"
+ """Mode to stream output, defaults to 'values'."""
+
+ stream_eager: bool = False
+ """Whether to force emitting stream events eagerly, automatically turned on
+ for stream_mode "messages" and "custom"."""
+
+ output_channels: str | Sequence[str]
+
+ stream_channels: str | Sequence[str] | None = None
+ """Channels to stream, defaults to all channels not in reserved channels"""
+
+ interrupt_after_nodes: All | Sequence[str]
+
+ interrupt_before_nodes: All | Sequence[str]
+
+ input_channels: str | Sequence[str]
+
+ step_timeout: float | None = None
+ """Maximum time to wait for a step to complete, in seconds."""
+
+ debug: bool
+ """Whether to print debug information during execution."""
+
+ checkpointer: Checkpointer = None
+ """`Checkpointer` used to save and load graph state."""
+
+ store: BaseStore | None = None
+ """Memory store to use for SharedValues."""
+
+ cache: BaseCache | None = None
+ """Cache to use for storing node results."""
+
+ retry_policy: Sequence[RetryPolicy] = ()
+ """Retry policies to use when running tasks. Empty set disables retries."""
+
+ cache_policy: CachePolicy | None = None
+ """Cache policy to use for all nodes. Can be overridden by individual nodes."""
+
+ context_schema: type[ContextT] | None = None
+ """Specifies the schema for the context object that will be passed to the workflow."""
+
+ config: RunnableConfig | None = None
+
+ name: str = "LangGraph"
+
+ trigger_to_nodes: Mapping[str, Sequence[str]]
+
+ def __init__(
+ self,
+ *,
+ nodes: dict[str, PregelNode | NodeBuilder],
+ channels: dict[str, BaseChannel | ManagedValueSpec] | None,
+ auto_validate: bool = True,
+ stream_mode: StreamMode = "values",
+ stream_eager: bool = False,
+ output_channels: str | Sequence[str],
+ stream_channels: str | Sequence[str] | None = None,
+ interrupt_after_nodes: All | Sequence[str] = (),
+ interrupt_before_nodes: All | Sequence[str] = (),
+ input_channels: str | Sequence[str],
+ step_timeout: float | None = None,
+ debug: bool | None = None,
+ checkpointer: Checkpointer = None,
+ store: BaseStore | None = None,
+ cache: BaseCache | None = None,
+ retry_policy: RetryPolicy | Sequence[RetryPolicy] = (),
+ cache_policy: CachePolicy | None = None,
+ context_schema: type[ContextT] | None = None,
+ config: RunnableConfig | None = None,
+ trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
+ name: str = "LangGraph",
+ **deprecated_kwargs: Unpack[DeprecatedKwargs],
+ ) -> None:
+ if (
+ config_type := deprecated_kwargs.get("config_type", MISSING)
+ ) is not MISSING:
+ warnings.warn(
+ "`config_type` is deprecated and will be removed. Please use `context_schema` instead.",
+ category=LangGraphDeprecatedSinceV10,
+ stacklevel=2,
+ )
+
+ if context_schema is None:
+ context_schema = cast(type[ContextT], config_type)
+
+ checkpointer = ensure_valid_checkpointer(checkpointer)
+
+ self.nodes = {
+ k: v.build() if isinstance(v, NodeBuilder) else v for k, v in nodes.items()
+ }
+ self.channels = channels or {}
+ if TASKS in self.channels and not isinstance(self.channels[TASKS], Topic):
+ raise ValueError(
+ f"Channel '{TASKS}' is reserved and cannot be used in the graph."
+ )
+ else:
+ self.channels[TASKS] = Topic(Send, accumulate=False)
+ self.stream_mode = stream_mode
+ self.stream_eager = stream_eager
+ self.output_channels = output_channels
+ self.stream_channels = stream_channels
+ self.interrupt_after_nodes = interrupt_after_nodes
+ self.interrupt_before_nodes = interrupt_before_nodes
+ self.input_channels = input_channels
+ self.step_timeout = step_timeout
+ self.debug = debug if debug is not None else get_debug()
+ self.checkpointer = checkpointer
+ self.store = store
+ self.cache = cache
+ self.retry_policy = (
+ (retry_policy,) if isinstance(retry_policy, RetryPolicy) else retry_policy
+ )
+ self.cache_policy = cache_policy
+ self.context_schema = context_schema
+ self.config = config
+ self.trigger_to_nodes = trigger_to_nodes or {}
+ self.name = name
+ if auto_validate:
+ self.validate()
+
+ def get_graph(
+ self, config: RunnableConfig | None = None, *, xray: int | bool = False
+ ) -> Graph:
+ """Return a drawable representation of the computation graph."""
+ # gather subgraphs
+ if xray:
+ subgraphs = {
+ k: v.get_graph(
+ config,
+ xray=xray if isinstance(xray, bool) or xray <= 0 else xray - 1,
+ )
+ for k, v in self.get_subgraphs()
+ }
+ else:
+ subgraphs = {}
+
+ return draw_graph(
+ merge_configs(self.config, config),
+ nodes=self.nodes,
+ specs=self.channels,
+ input_channels=self.input_channels,
+ interrupt_after_nodes=self.interrupt_after_nodes,
+ interrupt_before_nodes=self.interrupt_before_nodes,
+ trigger_to_nodes=self.trigger_to_nodes,
+ checkpointer=self.checkpointer,
+ subgraphs=subgraphs,
+ )
+
+ async def aget_graph(
+ self, config: RunnableConfig | None = None, *, xray: int | bool = False
+ ) -> Graph:
+ """Return a drawable representation of the computation graph."""
+
+ # gather subgraphs
+ if xray:
+ subpregels: dict[str, PregelProtocol] = {
+ k: v async for k, v in self.aget_subgraphs()
+ }
+ subgraphs = {
+ k: v
+ for k, v in zip(
+ subpregels,
+ await asyncio.gather(
+ *(
+ p.aget_graph(
+ config,
+ xray=xray
+ if isinstance(xray, bool) or xray <= 0
+ else xray - 1,
+ )
+ for p in subpregels.values()
+ )
+ ),
+ )
+ }
+ else:
+ subgraphs = {}
+
+ return draw_graph(
+ merge_configs(self.config, config),
+ nodes=self.nodes,
+ specs=self.channels,
+ input_channels=self.input_channels,
+ interrupt_after_nodes=self.interrupt_after_nodes,
+ interrupt_before_nodes=self.interrupt_before_nodes,
+ trigger_to_nodes=self.trigger_to_nodes,
+ checkpointer=self.checkpointer,
+ subgraphs=subgraphs,
+ )
+
+ def _repr_mimebundle_(self, **kwargs: Any) -> dict[str, Any]:
+ """Mime bundle used by Jupyter to display the graph"""
+ return {
+ "text/plain": repr(self),
+ "image/png": self.get_graph().draw_mermaid_png(),
+ }
+
+ def copy(self, update: dict[str, Any] | None = None) -> Self:
+ attrs = {k: v for k, v in self.__dict__.items() if k != "__orig_class__"}
+ attrs.update(update or {})
+ return self.__class__(**attrs)
+
+ def with_config(self, config: RunnableConfig | None = None, **kwargs: Any) -> Self:
+ """Create a copy of the Pregel object with an updated config."""
+ return self.copy(
+ {"config": merge_configs(self.config, config, cast(RunnableConfig, kwargs))}
+ )
+
+ def validate(self) -> Self:
+ validate_graph(
+ self.nodes,
+ {k: v for k, v in self.channels.items() if isinstance(v, BaseChannel)},
+ {k: v for k, v in self.channels.items() if not isinstance(v, BaseChannel)},
+ self.input_channels,
+ self.output_channels,
+ self.stream_channels,
+ self.interrupt_after_nodes,
+ self.interrupt_before_nodes,
+ )
+ self.trigger_to_nodes = _trigger_to_nodes(self.nodes)
+ return self
+
+ @deprecated(
+ "`config_schema` is deprecated. Use `get_context_jsonschema` for the relevant schema instead.",
+ category=None,
+ )
+ def config_schema(self, *, include: Sequence[str] | None = None) -> type[BaseModel]:
+ warnings.warn(
+ "`config_schema` is deprecated. Use `get_context_jsonschema` for the relevant schema instead.",
+ category=LangGraphDeprecatedSinceV10,
+ stacklevel=2,
+ )
+
+ include = include or []
+ fields = {
+ **(
+ {"configurable": (self.context_schema, None)}
+ if self.context_schema
+ else {}
+ ),
+ **{
+ field_name: (field_type, None)
+ for field_name, field_type in get_type_hints(RunnableConfig).items()
+ if field_name in [i for i in include if i != "configurable"]
+ },
+ }
+ return create_model(self.get_name("Config"), field_definitions=fields)
+
+ @deprecated(
+ "`get_config_jsonschema` is deprecated. Use `get_context_jsonschema` instead.",
+ category=None,
+ )
+ def get_config_jsonschema(
+ self, *, include: Sequence[str] | None = None
+ ) -> dict[str, Any]:
+ warnings.warn(
+ "`get_config_jsonschema` is deprecated. Use `get_context_jsonschema` instead.",
+ category=LangGraphDeprecatedSinceV10,
+ stacklevel=2,
+ )
+
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", category=LangGraphDeprecatedSinceV10)
+ schema = self.config_schema(include=include)
+ return schema.model_json_schema()
+
+ def get_context_jsonschema(self) -> dict[str, Any] | None:
+ if (context_schema := self.context_schema) is None:
+ return None
+
+ if isclass(context_schema) and issubclass(context_schema, BaseModel):
+ return context_schema.model_json_schema()
+ elif is_typeddict(context_schema) or is_dataclass(context_schema):
+ return TypeAdapter(context_schema).json_schema()
+ else:
+ raise ValueError(
+ f"Invalid context schema type: {context_schema}. Must be a BaseModel, TypedDict or dataclass."
+ )
+
+ @property
+ def InputType(self) -> Any:
+ if isinstance(self.input_channels, str):
+ channel = self.channels[self.input_channels]
+ if isinstance(channel, BaseChannel):
+ return channel.UpdateType
+
+ def get_input_schema(self, config: RunnableConfig | None = None) -> type[BaseModel]:
+ config = merge_configs(self.config, config)
+ if isinstance(self.input_channels, str):
+ return super().get_input_schema(config)
+ else:
+ return create_model(
+ self.get_name("Input"),
+ field_definitions={
+ k: (c.UpdateType, None)
+ for k in self.input_channels or self.channels.keys()
+ if (c := self.channels[k]) and isinstance(c, BaseChannel)
+ },
+ )
+
+ def get_input_jsonschema(
+ self, config: RunnableConfig | None = None
+ ) -> dict[str, Any]:
+ schema = self.get_input_schema(config)
+ return schema.model_json_schema()
+
+ @property
+ def OutputType(self) -> Any:
+ if isinstance(self.output_channels, str):
+ channel = self.channels[self.output_channels]
+ if isinstance(channel, BaseChannel):
+ return channel.ValueType
+
+ def get_output_schema(
+ self, config: RunnableConfig | None = None
+ ) -> type[BaseModel]:
+ config = merge_configs(self.config, config)
+ if isinstance(self.output_channels, str):
+ return super().get_output_schema(config)
+ else:
+ return create_model(
+ self.get_name("Output"),
+ field_definitions={
+ k: (c.ValueType, None)
+ for k in self.output_channels
+ if (c := self.channels[k]) and isinstance(c, BaseChannel)
+ },
+ )
+
+ def get_output_jsonschema(
+ self, config: RunnableConfig | None = None
+ ) -> dict[str, Any]:
+ schema = self.get_output_schema(config)
+ return schema.model_json_schema()
+
+ @property
+ def stream_channels_list(self) -> Sequence[str]:
+ stream_channels = self.stream_channels_asis
+ return (
+ [stream_channels] if isinstance(stream_channels, str) else stream_channels
+ )
+
+ @property
+ def stream_channels_asis(self) -> str | Sequence[str]:
+ return self.stream_channels or [
+ k for k in self.channels if isinstance(self.channels[k], BaseChannel)
+ ]
+
+ def get_subgraphs(
+ self, *, namespace: str | None = None, recurse: bool = False
+ ) -> Iterator[tuple[str, PregelProtocol]]:
+ """Get the subgraphs of the graph.
+
+ Args:
+ namespace: The namespace to filter the subgraphs by.
+ recurse: Whether to recurse into the subgraphs.
+ If `False`, only the immediate subgraphs will be returned.
+
+ Returns:
+ An iterator of the `(namespace, subgraph)` pairs.
+ """
+ for name, node in self.nodes.items():
+ # filter by prefix
+ if namespace is not None:
+ if not namespace.startswith(name):
+ continue
+
+ # find the subgraph, if any
+ graph = node.subgraphs[0] if node.subgraphs else None
+
+ # if found, yield recursively
+ if graph:
+ if name == namespace:
+ yield name, graph
+ return # we found it, stop searching
+ if namespace is None:
+ yield name, graph
+ if recurse and isinstance(graph, Pregel):
+ if namespace is not None:
+ namespace = namespace[len(name) + 1 :]
+ yield from (
+ (f"{name}{NS_SEP}{n}", s)
+ for n, s in graph.get_subgraphs(
+ namespace=namespace, recurse=recurse
+ )
+ )
+
+ async def aget_subgraphs(
+ self, *, namespace: str | None = None, recurse: bool = False
+ ) -> AsyncIterator[tuple[str, PregelProtocol]]:
+ """Get the subgraphs of the graph.
+
+ Args:
+ namespace: The namespace to filter the subgraphs by.
+ recurse: Whether to recurse into the subgraphs.
+ If `False`, only the immediate subgraphs will be returned.
+
+ Returns:
+ An iterator of the `(namespace, subgraph)` pairs.
+ """
+ for name, node in self.get_subgraphs(namespace=namespace, recurse=recurse):
+ yield name, node
+
+ def _migrate_checkpoint(self, checkpoint: Checkpoint) -> None:
+ """Migrate a saved checkpoint to new channel layout."""
+ if checkpoint["v"] < 4 and checkpoint.get("pending_sends"):
+ pending_sends: list[Send] = checkpoint.pop("pending_sends")
+ checkpoint["channel_values"][TASKS] = pending_sends
+ checkpoint["channel_versions"][TASKS] = max(
+ checkpoint["channel_versions"].values()
+ )
+
+ def _prepare_state_snapshot(
+ self,
+ config: RunnableConfig,
+ saved: CheckpointTuple | None,
+ recurse: BaseCheckpointSaver | None = None,
+ apply_pending_writes: bool = False,
+ ) -> StateSnapshot:
+ if not saved:
+ return StateSnapshot(
+ values={},
+ next=(),
+ config=config,
+ metadata=None,
+ created_at=None,
+ parent_config=None,
+ tasks=(),
+ interrupts=(),
+ )
+
+ # migrate checkpoint if needed
+ self._migrate_checkpoint(saved.checkpoint)
+
+ step = saved.metadata.get("step", -1) + 1
+ stop = step + 2
+ channels, managed = channels_from_checkpoint(
+ self.channels,
+ saved.checkpoint,
+ )
+ # tasks for this checkpoint
+ next_tasks = prepare_next_tasks(
+ saved.checkpoint,
+ saved.pending_writes or [],
+ self.nodes,
+ channels,
+ managed,
+ saved.config,
+ step,
+ stop,
+ for_execution=True,
+ store=self.store,
+ checkpointer=(
+ self.checkpointer
+ if isinstance(self.checkpointer, BaseCheckpointSaver)
+ else None
+ ),
+ manager=None,
+ )
+ # get the subgraphs
+ subgraphs = dict(self.get_subgraphs())
+ parent_ns = saved.config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")
+ task_states: dict[str, RunnableConfig | StateSnapshot] = {}
+ for task in next_tasks.values():
+ if task.name not in subgraphs:
+ continue
+ # assemble checkpoint_ns for this task
+ task_ns = f"{task.name}{NS_END}{task.id}"
+ if parent_ns:
+ task_ns = f"{parent_ns}{NS_SEP}{task_ns}"
+ if not recurse:
+ # set config as signal that subgraph checkpoints exist
+ config = {
+ CONF: {
+ "thread_id": saved.config[CONF]["thread_id"],
+ CONFIG_KEY_CHECKPOINT_NS: task_ns,
+ }
+ }
+ task_states[task.id] = config
+ else:
+ # get the state of the subgraph
+ config = {
+ CONF: {
+ CONFIG_KEY_CHECKPOINTER: recurse,
+ "thread_id": saved.config[CONF]["thread_id"],
+ CONFIG_KEY_CHECKPOINT_NS: task_ns,
+ }
+ }
+ task_states[task.id] = subgraphs[task.name].get_state(
+ config, subgraphs=True
+ )
+ # apply pending writes
+ if null_writes := [
+ w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID
+ ]:
+ apply_writes(
+ saved.checkpoint,
+ channels,
+ [PregelTaskWrites((), INPUT, null_writes, [])],
+ None,
+ self.trigger_to_nodes,
+ )
+ if apply_pending_writes and saved.pending_writes:
+ for tid, k, v in saved.pending_writes:
+ if k in (ERROR, INTERRUPT):
+ continue
+ if tid not in next_tasks:
+ continue
+ next_tasks[tid].writes.append((k, v))
+ if tasks := [t for t in next_tasks.values() if t.writes]:
+ apply_writes(
+ saved.checkpoint, channels, tasks, None, self.trigger_to_nodes
+ )
+ tasks_with_writes = tasks_w_writes(
+ next_tasks.values(),
+ saved.pending_writes,
+ task_states,
+ self.stream_channels_asis,
+ )
+ # assemble the state snapshot
+ return StateSnapshot(
+ read_channels(channels, self.stream_channels_asis),
+ tuple(t.name for t in next_tasks.values() if not t.writes),
+ patch_checkpoint_map(saved.config, saved.metadata),
+ saved.metadata,
+ saved.checkpoint["ts"],
+ patch_checkpoint_map(saved.parent_config, saved.metadata),
+ tasks_with_writes,
+ tuple([i for task in tasks_with_writes for i in task.interrupts]),
+ )
+
+ async def _aprepare_state_snapshot(
+ self,
+ config: RunnableConfig,
+ saved: CheckpointTuple | None,
+ recurse: BaseCheckpointSaver | None = None,
+ apply_pending_writes: bool = False,
+ ) -> StateSnapshot:
+ if not saved:
+ return StateSnapshot(
+ values={},
+ next=(),
+ config=config,
+ metadata=None,
+ created_at=None,
+ parent_config=None,
+ tasks=(),
+ interrupts=(),
+ )
+
+ # migrate checkpoint if needed
+ self._migrate_checkpoint(saved.checkpoint)
+
+ step = saved.metadata.get("step", -1) + 1
+ stop = step + 2
+ channels, managed = channels_from_checkpoint(
+ self.channels,
+ saved.checkpoint,
+ )
+ # tasks for this checkpoint
+ next_tasks = prepare_next_tasks(
+ saved.checkpoint,
+ saved.pending_writes or [],
+ self.nodes,
+ channels,
+ managed,
+ saved.config,
+ step,
+ stop,
+ for_execution=True,
+ store=self.store,
+ checkpointer=(
+ self.checkpointer
+ if isinstance(self.checkpointer, BaseCheckpointSaver)
+ else None
+ ),
+ manager=None,
+ )
+ # get the subgraphs
+ subgraphs = {n: g async for n, g in self.aget_subgraphs()}
+ parent_ns = saved.config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")
+ task_states: dict[str, RunnableConfig | StateSnapshot] = {}
+ for task in next_tasks.values():
+ if task.name not in subgraphs:
+ continue
+ # assemble checkpoint_ns for this task
+ task_ns = f"{task.name}{NS_END}{task.id}"
+ if parent_ns:
+ task_ns = f"{parent_ns}{NS_SEP}{task_ns}"
+ if not recurse:
+ # set config as signal that subgraph checkpoints exist
+ config = {
+ CONF: {
+ "thread_id": saved.config[CONF]["thread_id"],
+ CONFIG_KEY_CHECKPOINT_NS: task_ns,
+ }
+ }
+ task_states[task.id] = config
+ else:
+ # get the state of the subgraph
+ config = {
+ CONF: {
+ CONFIG_KEY_CHECKPOINTER: recurse,
+ "thread_id": saved.config[CONF]["thread_id"],
+ CONFIG_KEY_CHECKPOINT_NS: task_ns,
+ }
+ }
+ task_states[task.id] = await subgraphs[task.name].aget_state(
+ config, subgraphs=True
+ )
+ # apply pending writes
+ if null_writes := [
+ w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID
+ ]:
+ apply_writes(
+ saved.checkpoint,
+ channels,
+ [PregelTaskWrites((), INPUT, null_writes, [])],
+ None,
+ self.trigger_to_nodes,
+ )
+ if apply_pending_writes and saved.pending_writes:
+ for tid, k, v in saved.pending_writes:
+ if k in (ERROR, INTERRUPT):
+ continue
+ if tid not in next_tasks:
+ continue
+ next_tasks[tid].writes.append((k, v))
+ if tasks := [t for t in next_tasks.values() if t.writes]:
+ apply_writes(
+ saved.checkpoint, channels, tasks, None, self.trigger_to_nodes
+ )
+
+ tasks_with_writes = tasks_w_writes(
+ next_tasks.values(),
+ saved.pending_writes,
+ task_states,
+ self.stream_channels_asis,
+ )
+ # assemble the state snapshot
+ return StateSnapshot(
+ read_channels(channels, self.stream_channels_asis),
+ tuple(t.name for t in next_tasks.values() if not t.writes),
+ patch_checkpoint_map(saved.config, saved.metadata),
+ saved.metadata,
+ saved.checkpoint["ts"],
+ patch_checkpoint_map(saved.parent_config, saved.metadata),
+ tasks_with_writes,
+ tuple([i for task in tasks_with_writes for i in task.interrupts]),
+ )
+
+ def get_state(
+ self, config: RunnableConfig, *, subgraphs: bool = False
+ ) -> StateSnapshot:
+ """Get the current state of the graph."""
+ checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
+ CONFIG_KEY_CHECKPOINTER, self.checkpointer
+ )
+ if not checkpointer:
+ raise ValueError("No checkpointer set")
+
+ if (
+ checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")
+ ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]:
+ # remove task_ids from checkpoint_ns
+ recast = recast_checkpoint_ns(checkpoint_ns)
+ # find the subgraph with the matching name
+ for _, pregel in self.get_subgraphs(namespace=recast, recurse=True):
+ return pregel.get_state(
+ patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}),
+ subgraphs=subgraphs,
+ )
+ else:
+ raise ValueError(f"Subgraph {recast} not found")
+
+ config = merge_configs(self.config, config) if self.config else config
+ if self.checkpointer is True:
+ ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS])
+ config = merge_configs(
+ config, {CONF: {CONFIG_KEY_CHECKPOINT_NS: recast_checkpoint_ns(ns)}}
+ )
+ thread_id = config[CONF][CONFIG_KEY_THREAD_ID]
+ if not isinstance(thread_id, str):
+ config[CONF][CONFIG_KEY_THREAD_ID] = str(thread_id)
+
+ saved = checkpointer.get_tuple(config)
+ return self._prepare_state_snapshot(
+ config,
+ saved,
+ recurse=checkpointer if subgraphs else None,
+ apply_pending_writes=CONFIG_KEY_CHECKPOINT_ID not in config[CONF],
+ )
+
+ async def aget_state(
+ self, config: RunnableConfig, *, subgraphs: bool = False
+ ) -> StateSnapshot:
+ """Get the current state of the graph."""
+ checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
+ CONFIG_KEY_CHECKPOINTER, self.checkpointer
+ )
+ if not checkpointer:
+ raise ValueError("No checkpointer set")
+
+ if (
+ checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")
+ ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]:
+ # remove task_ids from checkpoint_ns
+ recast = recast_checkpoint_ns(checkpoint_ns)
+ # find the subgraph with the matching name
+ async for _, pregel in self.aget_subgraphs(namespace=recast, recurse=True):
+ return await pregel.aget_state(
+ patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}),
+ subgraphs=subgraphs,
+ )
+ else:
+ raise ValueError(f"Subgraph {recast} not found")
+
+ config = merge_configs(self.config, config) if self.config else config
+ if self.checkpointer is True:
+ ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS])
+ config = merge_configs(
+ config, {CONF: {CONFIG_KEY_CHECKPOINT_NS: recast_checkpoint_ns(ns)}}
+ )
+ thread_id = config[CONF][CONFIG_KEY_THREAD_ID]
+ if not isinstance(thread_id, str):
+ config[CONF][CONFIG_KEY_THREAD_ID] = str(thread_id)
+
+ saved = await checkpointer.aget_tuple(config)
+ return await self._aprepare_state_snapshot(
+ config,
+ saved,
+ recurse=checkpointer if subgraphs else None,
+ apply_pending_writes=CONFIG_KEY_CHECKPOINT_ID not in config[CONF],
+ )
+
+ def get_state_history(
+ self,
+ config: RunnableConfig,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> Iterator[StateSnapshot]:
+ """Get the history of the state of the graph."""
+ config = ensure_config(config)
+ checkpointer: BaseCheckpointSaver | None = config[CONF].get(
+ CONFIG_KEY_CHECKPOINTER, self.checkpointer
+ )
+ if not checkpointer:
+ raise ValueError("No checkpointer set")
+
+ if (
+ checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")
+ ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]:
+ # remove task_ids from checkpoint_ns
+ recast = recast_checkpoint_ns(checkpoint_ns)
+ # find the subgraph with the matching name
+ for _, pregel in self.get_subgraphs(namespace=recast, recurse=True):
+ yield from pregel.get_state_history(
+ patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}),
+ filter=filter,
+ before=before,
+ limit=limit,
+ )
+ return
+ else:
+ raise ValueError(f"Subgraph {recast} not found")
+
+ config = merge_configs(
+ self.config,
+ config,
+ {
+ CONF: {
+ CONFIG_KEY_CHECKPOINT_NS: checkpoint_ns,
+ CONFIG_KEY_THREAD_ID: str(config[CONF][CONFIG_KEY_THREAD_ID]),
+ }
+ },
+ )
+ # eagerly consume list() to avoid holding up the db cursor
+ for checkpoint_tuple in list(
+ checkpointer.list(config, before=before, limit=limit, filter=filter)
+ ):
+ yield self._prepare_state_snapshot(
+ checkpoint_tuple.config, checkpoint_tuple
+ )
+
+ async def aget_state_history(
+ self,
+ config: RunnableConfig,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> AsyncIterator[StateSnapshot]:
+ """Asynchronously get the history of the state of the graph."""
+ config = ensure_config(config)
+ checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
+ CONFIG_KEY_CHECKPOINTER, self.checkpointer
+ )
+ if not checkpointer:
+ raise ValueError("No checkpointer set")
+
+ if (
+ checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")
+ ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]:
+ # remove task_ids from checkpoint_ns
+ recast = recast_checkpoint_ns(checkpoint_ns)
+ # find the subgraph with the matching name
+ async for _, pregel in self.aget_subgraphs(namespace=recast, recurse=True):
+ async for state in pregel.aget_state_history(
+ patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}),
+ filter=filter,
+ before=before,
+ limit=limit,
+ ):
+ yield state
+ return
+ else:
+ raise ValueError(f"Subgraph {recast} not found")
+
+ config = merge_configs(
+ self.config,
+ config,
+ {
+ CONF: {
+ CONFIG_KEY_CHECKPOINT_NS: checkpoint_ns,
+ CONFIG_KEY_THREAD_ID: str(config[CONF][CONFIG_KEY_THREAD_ID]),
+ }
+ },
+ )
+ # eagerly consume list() to avoid holding up the db cursor
+ for checkpoint_tuple in [
+ c
+ async for c in checkpointer.alist(
+ config, before=before, limit=limit, filter=filter
+ )
+ ]:
+ yield await self._aprepare_state_snapshot(
+ checkpoint_tuple.config, checkpoint_tuple
+ )
+
+ def bulk_update_state(
+ self,
+ config: RunnableConfig,
+ supersteps: Sequence[Sequence[StateUpdate]],
+ ) -> RunnableConfig:
+ """Apply updates to the graph state in bulk. Requires a checkpointer to be set.
+
+ Args:
+ config: The config to apply the updates to.
+ supersteps: A list of supersteps, each including a list of updates to apply sequentially to a graph state.
+
+ Each update is a tuple of the form `(values, as_node, task_id)` where `task_id` is optional.
+
+ Raises:
+ ValueError: If no checkpointer is set or no updates are provided.
+ InvalidUpdateError: If an invalid update is provided.
+
+ Returns:
+ RunnableConfig: The updated config.
+ """
+
+ checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
+ CONFIG_KEY_CHECKPOINTER, self.checkpointer
+ )
+ if not checkpointer:
+ raise ValueError("No checkpointer set")
+
+ if len(supersteps) == 0:
+ raise ValueError("No supersteps provided")
+
+ if any(len(u) == 0 for u in supersteps):
+ raise ValueError("No updates provided")
+
+ # delegate to subgraph
+ if (
+ checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")
+ ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]:
+ # remove task_ids from checkpoint_ns
+ recast = recast_checkpoint_ns(checkpoint_ns)
+ # find the subgraph with the matching name
+ for _, pregel in self.get_subgraphs(namespace=recast, recurse=True):
+ return pregel.bulk_update_state(
+ patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}),
+ supersteps,
+ )
+ else:
+ raise ValueError(f"Subgraph {recast} not found")
+
+ def perform_superstep(
+ input_config: RunnableConfig, updates: Sequence[StateUpdate]
+ ) -> RunnableConfig:
+ # get last checkpoint
+ config = ensure_config(self.config, input_config)
+ saved = checkpointer.get_tuple(config)
+ if saved is not None:
+ self._migrate_checkpoint(saved.checkpoint)
+ checkpoint = (
+ copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint()
+ )
+ checkpoint_previous_versions = (
+ saved.checkpoint["channel_versions"].copy() if saved else {}
+ )
+ step = saved.metadata.get("step", -1) if saved else -1
+ # merge configurable fields with previous checkpoint config
+ checkpoint_config = patch_configurable(
+ config,
+ {
+ CONFIG_KEY_CHECKPOINT_NS: config[CONF].get(
+ CONFIG_KEY_CHECKPOINT_NS, ""
+ )
+ },
+ )
+ if saved:
+ checkpoint_config = patch_configurable(config, saved.config[CONF])
+ channels, managed = channels_from_checkpoint(
+ self.channels,
+ checkpoint,
+ )
+ values, as_node = updates[0][:2]
+
+ # no values as END, just clear all tasks
+ if values is None and as_node == END:
+ if len(updates) > 1:
+ raise InvalidUpdateError(
+ "Cannot apply multiple updates when clearing state"
+ )
+
+ if saved is not None:
+ # tasks for this checkpoint
+ next_tasks = prepare_next_tasks(
+ checkpoint,
+ saved.pending_writes or [],
+ self.nodes,
+ channels,
+ managed,
+ saved.config,
+ step + 1,
+ step + 3,
+ for_execution=True,
+ store=self.store,
+ checkpointer=checkpointer,
+ manager=None,
+ )
+ # apply null writes
+ if null_writes := [
+ w[1:]
+ for w in saved.pending_writes or []
+ if w[0] == NULL_TASK_ID
+ ]:
+ apply_writes(
+ checkpoint,
+ channels,
+ [PregelTaskWrites((), INPUT, null_writes, [])],
+ checkpointer.get_next_version,
+ self.trigger_to_nodes,
+ )
+ # apply writes from tasks that already ran
+ for tid, k, v in saved.pending_writes or []:
+ if k in (ERROR, INTERRUPT):
+ continue
+ if tid not in next_tasks:
+ continue
+ next_tasks[tid].writes.append((k, v))
+ # clear all current tasks
+ apply_writes(
+ checkpoint,
+ channels,
+ next_tasks.values(),
+ checkpointer.get_next_version,
+ self.trigger_to_nodes,
+ )
+ # save checkpoint
+ next_config = checkpointer.put(
+ checkpoint_config,
+ create_checkpoint(checkpoint, channels, step),
+ {
+ "source": "update",
+ "step": step + 1,
+ "parents": saved.metadata.get("parents", {}) if saved else {},
+ },
+ get_new_channel_versions(
+ checkpoint_previous_versions,
+ checkpoint["channel_versions"],
+ ),
+ )
+ return patch_checkpoint_map(
+ next_config, saved.metadata if saved else None
+ )
+
+ # act as an input
+ if as_node == INPUT:
+ if len(updates) > 1:
+ raise InvalidUpdateError(
+ "Cannot apply multiple updates when updating as input"
+ )
+
+ if input_writes := deque(map_input(self.input_channels, values)):
+ apply_writes(
+ checkpoint,
+ channels,
+ [PregelTaskWrites((), INPUT, input_writes, [])],
+ checkpointer.get_next_version,
+ self.trigger_to_nodes,
+ )
+
+ # apply input write to channels
+ next_step = (
+ step + 1
+ if saved and saved.metadata.get("step") is not None
+ else -1
+ )
+ next_config = checkpointer.put(
+ checkpoint_config,
+ create_checkpoint(checkpoint, channels, next_step),
+ {
+ "source": "input",
+ "step": next_step,
+ "parents": saved.metadata.get("parents", {})
+ if saved
+ else {},
+ },
+ get_new_channel_versions(
+ checkpoint_previous_versions,
+ checkpoint["channel_versions"],
+ ),
+ )
+
+ # store the writes
+ checkpointer.put_writes(
+ next_config,
+ input_writes,
+ str(uuid5(UUID(checkpoint["id"]), INPUT)),
+ )
+
+ return patch_checkpoint_map(
+ next_config, saved.metadata if saved else None
+ )
+ else:
+ raise InvalidUpdateError(
+ f"Received no input writes for {self.input_channels}"
+ )
+
+ # copy checkpoint
+ if as_node == "__copy__":
+ if len(updates) > 1:
+ raise InvalidUpdateError(
+ "Cannot copy checkpoint with multiple updates"
+ )
+
+ if saved is None:
+ raise InvalidUpdateError("Cannot copy a non-existent checkpoint")
+
+ next_checkpoint = create_checkpoint(checkpoint, None, step)
+
+ # copy checkpoint
+ next_config = checkpointer.put(
+ saved.parent_config
+ or patch_configurable(
+ saved.config, {CONFIG_KEY_CHECKPOINT_ID: None}
+ ),
+ next_checkpoint,
+ {
+ "source": "fork",
+ "step": step + 1,
+ "parents": saved.metadata.get("parents", {}),
+ },
+ {},
+ )
+
+ # we want to both clone a checkpoint and update state in one go.
+ # reuse the same task ID if possible.
+ if isinstance(values, list) and len(values) > 0:
+ # figure out the task IDs for the next update checkpoint
+ next_tasks = prepare_next_tasks(
+ next_checkpoint,
+ saved.pending_writes or [],
+ self.nodes,
+ channels,
+ managed,
+ next_config,
+ step + 2,
+ step + 4,
+ for_execution=True,
+ store=self.store,
+ checkpointer=checkpointer,
+ manager=None,
+ )
+
+ tasks_group_by = defaultdict(list)
+ user_group_by: dict[str, list[StateUpdate]] = defaultdict(list)
+
+ for task in next_tasks.values():
+ tasks_group_by[task.name].append(task.id)
+
+ for item in values:
+ if not isinstance(item, Sequence):
+ raise InvalidUpdateError(
+ f"Invalid update item: {item} when copying checkpoint"
+ )
+
+ values, as_node = item[:2]
+
+ user_group = user_group_by[as_node]
+ tasks_group = tasks_group_by[as_node]
+
+ target_idx = len(user_group)
+ task_id = (
+ tasks_group[target_idx]
+ if target_idx < len(tasks_group)
+ else None
+ )
+
+ user_group_by[as_node].append(
+ StateUpdate(values=values, as_node=as_node, task_id=task_id)
+ )
+
+ return perform_superstep(
+ patch_checkpoint_map(next_config, saved.metadata),
+ [item for lst in user_group_by.values() for item in lst],
+ )
+
+ return patch_checkpoint_map(next_config, saved.metadata)
+
+ # task ids can be provided in the StateUpdate, but if not,
+ # we use the task id generated by prepare_next_tasks
+ node_to_task_ids: dict[str, deque[str]] = defaultdict(deque)
+ if saved is not None and saved.pending_writes is not None:
+ # we call prepare_next_tasks to discover the task IDs that
+ # would have been generated, so we can reuse them and
+ # properly populate task.result in state history
+ next_tasks = prepare_next_tasks(
+ checkpoint,
+ saved.pending_writes,
+ self.nodes,
+ channels,
+ managed,
+ saved.config,
+ step + 1,
+ step + 3,
+ for_execution=True,
+ store=self.store,
+ checkpointer=checkpointer,
+ manager=None,
+ )
+ # collect task ids to reuse so we can properly attach task results
+ for t in next_tasks.values():
+ node_to_task_ids[t.name].append(t.id)
+
+ valid_updates: list[tuple[str, dict[str, Any] | None, str | None]] = []
+ if len(updates) == 1:
+ values, as_node, task_id = updates[0]
+ # find last node that updated the state, if not provided
+ if as_node is None and len(self.nodes) == 1:
+ as_node = tuple(self.nodes)[0]
+ elif as_node is None and not any(
+ v
+ for vv in checkpoint["versions_seen"].values()
+ for v in vv.values()
+ ):
+ if (
+ isinstance(self.input_channels, str)
+ and self.input_channels in self.nodes
+ ):
+ as_node = self.input_channels
+ elif as_node is None:
+ last_seen_by_node = sorted(
+ (v, n)
+ for n, seen in checkpoint["versions_seen"].items()
+ if n in self.nodes
+ for v in seen.values()
+ )
+ # if two nodes updated the state at the same time, it's ambiguous
+ if last_seen_by_node:
+ if len(last_seen_by_node) == 1:
+ as_node = last_seen_by_node[0][1]
+ elif last_seen_by_node[-1][0] != last_seen_by_node[-2][0]:
+ as_node = last_seen_by_node[-1][1]
+ if as_node is None:
+ raise InvalidUpdateError("Ambiguous update, specify as_node")
+ if as_node not in self.nodes:
+ raise InvalidUpdateError(f"Node {as_node} does not exist")
+ valid_updates.append((as_node, values, task_id))
+ else:
+ for values, as_node, task_id in updates:
+ if as_node is None:
+ raise InvalidUpdateError(
+ "as_node is required when applying multiple updates"
+ )
+ if as_node not in self.nodes:
+ raise InvalidUpdateError(f"Node {as_node} does not exist")
+
+ valid_updates.append((as_node, values, task_id))
+
+ run_tasks: list[PregelTaskWrites] = []
+ run_task_ids: list[str] = []
+
+ for as_node, values, provided_task_id in valid_updates:
+ # create task to run all writers of the chosen node
+ writers = self.nodes[as_node].flat_writers
+ if not writers:
+ raise InvalidUpdateError(f"Node {as_node} has no writers")
+ writes: deque[tuple[str, Any]] = deque()
+ task = PregelTaskWrites((), as_node, writes, [INTERRUPT])
+ # get the task ids that were prepared for this node
+ # if a task id was provided in the StateUpdate, we use it
+ # otherwise, we use the next available task id
+ prepared_task_ids = node_to_task_ids.get(as_node, deque())
+ task_id = provided_task_id or (
+ prepared_task_ids.popleft()
+ if prepared_task_ids
+ else str(uuid5(UUID(checkpoint["id"]), INTERRUPT))
+ )
+ run_tasks.append(task)
+ run_task_ids.append(task_id)
+ run = RunnableSequence(*writers) if len(writers) > 1 else writers[0]
+ # execute task
+ run.invoke(
+ values,
+ patch_config(
+ config,
+ run_name=self.name + "UpdateState",
+ configurable={
+ # deque.extend is thread-safe
+ CONFIG_KEY_SEND: writes.extend,
+ CONFIG_KEY_TASK_ID: task_id,
+ CONFIG_KEY_READ: partial(
+ local_read,
+ _scratchpad(
+ None,
+ [],
+ task_id,
+ "",
+ None,
+ step,
+ step + 2,
+ ),
+ channels,
+ managed,
+ task,
+ ),
+ },
+ ),
+ )
+ # save task writes
+ for task_id, task in zip(run_task_ids, run_tasks):
+ # channel writes are saved to current checkpoint
+ channel_writes = [w for w in task.writes if w[0] != PUSH]
+ if saved and channel_writes:
+ checkpointer.put_writes(checkpoint_config, channel_writes, task_id)
+ # apply to checkpoint and save
+ apply_writes(
+ checkpoint,
+ channels,
+ run_tasks,
+ checkpointer.get_next_version,
+ self.trigger_to_nodes,
+ )
+ checkpoint = create_checkpoint(checkpoint, channels, step + 1)
+ next_config = checkpointer.put(
+ checkpoint_config,
+ checkpoint,
+ {
+ "source": "update",
+ "step": step + 1,
+ "parents": saved.metadata.get("parents", {}) if saved else {},
+ },
+ get_new_channel_versions(
+ checkpoint_previous_versions, checkpoint["channel_versions"]
+ ),
+ )
+ for task_id, task in zip(run_task_ids, run_tasks):
+ # save push writes
+ if push_writes := [w for w in task.writes if w[0] == PUSH]:
+ checkpointer.put_writes(next_config, push_writes, task_id)
+
+ return patch_checkpoint_map(next_config, saved.metadata if saved else None)
+
+ current_config = patch_configurable(
+ config, {CONFIG_KEY_THREAD_ID: str(config[CONF][CONFIG_KEY_THREAD_ID])}
+ )
+ for superstep in supersteps:
+ current_config = perform_superstep(current_config, superstep)
+ return current_config
+
+ async def abulk_update_state(
+ self,
+ config: RunnableConfig,
+ supersteps: Sequence[Sequence[StateUpdate]],
+ ) -> RunnableConfig:
+ """Asynchronously apply updates to the graph state in bulk. Requires a checkpointer to be set.
+
+ Args:
+ config: The config to apply the updates to.
+ supersteps: A list of supersteps, each including a list of updates to apply sequentially to a graph state.
+
+ Each update is a tuple of the form `(values, as_node, task_id)` where `task_id` is optional.
+
+ Raises:
+ ValueError: If no checkpointer is set or no updates are provided.
+ InvalidUpdateError: If an invalid update is provided.
+
+ Returns:
+ RunnableConfig: The updated config.
+ """
+
+ checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get(
+ CONFIG_KEY_CHECKPOINTER, self.checkpointer
+ )
+ if not checkpointer:
+ raise ValueError("No checkpointer set")
+
+ if len(supersteps) == 0:
+ raise ValueError("No supersteps provided")
+
+ if any(len(u) == 0 for u in supersteps):
+ raise ValueError("No updates provided")
+
+ # delegate to subgraph
+ if (
+ checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")
+ ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]:
+ # remove task_ids from checkpoint_ns
+ recast = recast_checkpoint_ns(checkpoint_ns)
+ # find the subgraph with the matching name
+ async for _, pregel in self.aget_subgraphs(namespace=recast, recurse=True):
+ return await pregel.abulk_update_state(
+ patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}),
+ supersteps,
+ )
+ else:
+ raise ValueError(f"Subgraph {recast} not found")
+
+ async def aperform_superstep(
+ input_config: RunnableConfig, updates: Sequence[StateUpdate]
+ ) -> RunnableConfig:
+ # get last checkpoint
+ config = ensure_config(self.config, input_config)
+ saved = await checkpointer.aget_tuple(config)
+ if saved is not None:
+ self._migrate_checkpoint(saved.checkpoint)
+ checkpoint = (
+ copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint()
+ )
+ checkpoint_previous_versions = (
+ saved.checkpoint["channel_versions"].copy() if saved else {}
+ )
+ step = saved.metadata.get("step", -1) if saved else -1
+ # merge configurable fields with previous checkpoint config
+ checkpoint_config = patch_configurable(
+ config,
+ {
+ CONFIG_KEY_CHECKPOINT_NS: config[CONF].get(
+ CONFIG_KEY_CHECKPOINT_NS, ""
+ )
+ },
+ )
+ if saved:
+ checkpoint_config = patch_configurable(config, saved.config[CONF])
+ channels, managed = channels_from_checkpoint(
+ self.channels,
+ checkpoint,
+ )
+ values, as_node = updates[0][:2]
+ # no values, just clear all tasks
+ if values is None and as_node == END:
+ if len(updates) > 1:
+ raise InvalidUpdateError(
+ "Cannot apply multiple updates when clearing state"
+ )
+ if saved is not None:
+ # tasks for this checkpoint
+ next_tasks = prepare_next_tasks(
+ checkpoint,
+ saved.pending_writes or [],
+ self.nodes,
+ channels,
+ managed,
+ saved.config,
+ step + 1,
+ step + 3,
+ for_execution=True,
+ store=self.store,
+ checkpointer=checkpointer,
+ manager=None,
+ )
+ # apply null writes
+ if null_writes := [
+ w[1:]
+ for w in saved.pending_writes or []
+ if w[0] == NULL_TASK_ID
+ ]:
+ apply_writes(
+ checkpoint,
+ channels,
+ [PregelTaskWrites((), INPUT, null_writes, [])],
+ checkpointer.get_next_version,
+ self.trigger_to_nodes,
+ )
+ # apply writes from tasks that already ran
+ for tid, k, v in saved.pending_writes or []:
+ if k in (ERROR, INTERRUPT):
+ continue
+ if tid not in next_tasks:
+ continue
+ next_tasks[tid].writes.append((k, v))
+ # clear all current tasks
+ apply_writes(
+ checkpoint,
+ channels,
+ next_tasks.values(),
+ checkpointer.get_next_version,
+ self.trigger_to_nodes,
+ )
+ # save checkpoint
+ next_config = await checkpointer.aput(
+ checkpoint_config,
+ create_checkpoint(checkpoint, channels, step),
+ {
+ "source": "update",
+ "step": step + 1,
+ "parents": saved.metadata.get("parents", {}) if saved else {},
+ },
+ get_new_channel_versions(
+ checkpoint_previous_versions, checkpoint["channel_versions"]
+ ),
+ )
+ return patch_checkpoint_map(
+ next_config, saved.metadata if saved else None
+ )
+
+ # act as an input
+ if as_node == INPUT:
+ if len(updates) > 1:
+ raise InvalidUpdateError(
+ "Cannot apply multiple updates when updating as input"
+ )
+
+ if input_writes := deque(map_input(self.input_channels, values)):
+ apply_writes(
+ checkpoint,
+ channels,
+ [PregelTaskWrites((), INPUT, input_writes, [])],
+ checkpointer.get_next_version,
+ self.trigger_to_nodes,
+ )
+
+ # apply input write to channels
+ next_step = (
+ step + 1
+ if saved and saved.metadata.get("step") is not None
+ else -1
+ )
+ next_config = await checkpointer.aput(
+ checkpoint_config,
+ create_checkpoint(checkpoint, channels, next_step),
+ {
+ "source": "input",
+ "step": next_step,
+ "parents": saved.metadata.get("parents", {})
+ if saved
+ else {},
+ },
+ get_new_channel_versions(
+ checkpoint_previous_versions,
+ checkpoint["channel_versions"],
+ ),
+ )
+
+ # store the writes
+ await checkpointer.aput_writes(
+ next_config,
+ input_writes,
+ str(uuid5(UUID(checkpoint["id"]), INPUT)),
+ )
+
+ return patch_checkpoint_map(
+ next_config, saved.metadata if saved else None
+ )
+ else:
+ raise InvalidUpdateError(
+ f"Received no input writes for {self.input_channels}"
+ )
+
+ # no values, copy checkpoint
+ if as_node == "__copy__":
+ if len(updates) > 1:
+ raise InvalidUpdateError(
+ "Cannot copy checkpoint with multiple updates"
+ )
+
+ if saved is None:
+ raise InvalidUpdateError("Cannot copy a non-existent checkpoint")
+
+ next_checkpoint = create_checkpoint(checkpoint, None, step)
+
+ # copy checkpoint
+ next_config = await checkpointer.aput(
+ saved.parent_config
+ or patch_configurable(
+ saved.config, {CONFIG_KEY_CHECKPOINT_ID: None}
+ ),
+ next_checkpoint,
+ {
+ "source": "fork",
+ "step": step + 1,
+ "parents": saved.metadata.get("parents", {}),
+ },
+ {},
+ )
+
+ # we want to both clone a checkpoint and update state in one go.
+ # reuse the same task ID if possible.
+ if isinstance(values, list) and len(values) > 0:
+ # figure out the task IDs for the next update checkpoint
+ next_tasks = prepare_next_tasks(
+ next_checkpoint,
+ saved.pending_writes or [],
+ self.nodes,
+ channels,
+ managed,
+ next_config,
+ step + 2,
+ step + 4,
+ for_execution=True,
+ store=self.store,
+ checkpointer=checkpointer,
+ manager=None,
+ )
+
+ tasks_group_by = defaultdict(list)
+ user_group_by: dict[str, list[StateUpdate]] = defaultdict(list)
+
+ for task in next_tasks.values():
+ tasks_group_by[task.name].append(task.id)
+
+ for item in values:
+ if not isinstance(item, Sequence):
+ raise InvalidUpdateError(
+ f"Invalid update item: {item} when copying checkpoint"
+ )
+
+ values, as_node = item[:2]
+ user_group = user_group_by[as_node]
+ tasks_group = tasks_group_by[as_node]
+
+ target_idx = len(user_group)
+ task_id = (
+ tasks_group[target_idx]
+ if target_idx < len(tasks_group)
+ else None
+ )
+
+ user_group_by[as_node].append(
+ StateUpdate(values=values, as_node=as_node, task_id=task_id)
+ )
+
+ return await aperform_superstep(
+ patch_checkpoint_map(next_config, saved.metadata),
+ [item for lst in user_group_by.values() for item in lst],
+ )
+
+ return patch_checkpoint_map(
+ next_config, saved.metadata if saved else None
+ )
+
+ # task ids can be provided in the StateUpdate, but if not,
+ # we use the task id generated by prepare_next_tasks
+ node_to_task_ids: dict[str, deque[str]] = defaultdict(deque)
+ if saved is not None and saved.pending_writes is not None:
+ # we call prepare_next_tasks to discover the task IDs that
+ # would have been generated, so we can reuse them and
+ # properly populate task.result in state history
+ next_tasks = prepare_next_tasks(
+ checkpoint,
+ saved.pending_writes,
+ self.nodes,
+ channels,
+ managed,
+ saved.config,
+ step + 1,
+ step + 3,
+ for_execution=True,
+ store=self.store,
+ checkpointer=checkpointer,
+ manager=None,
+ )
+ # collect task ids to reuse so we can properly attach task results
+ for t in next_tasks.values():
+ node_to_task_ids[t.name].append(t.id)
+
+ valid_updates: list[tuple[str, dict[str, Any] | None, str | None]] = []
+ if len(updates) == 1:
+ values, as_node, task_id = updates[0]
+ # find last node that updated the state, if not provided
+ if as_node is None and len(self.nodes) == 1:
+ as_node = tuple(self.nodes)[0]
+ elif as_node is None and not saved:
+ if (
+ isinstance(self.input_channels, str)
+ and self.input_channels in self.nodes
+ ):
+ as_node = self.input_channels
+ elif as_node is None:
+ last_seen_by_node = sorted(
+ (v, n)
+ for n, seen in checkpoint["versions_seen"].items()
+ if n in self.nodes
+ for v in seen.values()
+ )
+ # if two nodes updated the state at the same time, it's ambiguous
+ if last_seen_by_node:
+ if len(last_seen_by_node) == 1:
+ as_node = last_seen_by_node[0][1]
+ elif last_seen_by_node[-1][0] != last_seen_by_node[-2][0]:
+ as_node = last_seen_by_node[-1][1]
+ if as_node is None:
+ raise InvalidUpdateError("Ambiguous update, specify as_node")
+ if as_node not in self.nodes:
+ raise InvalidUpdateError(f"Node {as_node} does not exist")
+ valid_updates.append((as_node, values, task_id))
+ else:
+ for values, as_node, task_id in updates:
+ if as_node is None:
+ raise InvalidUpdateError(
+ "as_node is required when applying multiple updates"
+ )
+ if as_node not in self.nodes:
+ raise InvalidUpdateError(f"Node {as_node} does not exist")
+
+ valid_updates.append((as_node, values, task_id))
+
+ run_tasks: list[PregelTaskWrites] = []
+ run_task_ids: list[str] = []
+
+ for as_node, values, provided_task_id in valid_updates:
+ # create task to run all writers of the chosen node
+ writers = self.nodes[as_node].flat_writers
+ if not writers:
+ raise InvalidUpdateError(f"Node {as_node} has no writers")
+ writes: deque[tuple[str, Any]] = deque()
+ task = PregelTaskWrites((), as_node, writes, [INTERRUPT])
+ # get the task ids that were prepared for this node
+ # if a task id was provided in the StateUpdate, we use it
+ # otherwise, we use the next available task id
+ prepared_task_ids = node_to_task_ids.get(as_node, deque())
+ task_id = provided_task_id or (
+ prepared_task_ids.popleft()
+ if prepared_task_ids
+ else str(uuid5(UUID(checkpoint["id"]), INTERRUPT))
+ )
+ run_tasks.append(task)
+ run_task_ids.append(task_id)
+ run = RunnableSequence(*writers) if len(writers) > 1 else writers[0]
+ # execute task
+ await run.ainvoke(
+ values,
+ patch_config(
+ config,
+ run_name=self.name + "UpdateState",
+ configurable={
+ # deque.extend is thread-safe
+ CONFIG_KEY_SEND: writes.extend,
+ CONFIG_KEY_TASK_ID: task_id,
+ CONFIG_KEY_READ: partial(
+ local_read,
+ _scratchpad(
+ None,
+ [],
+ task_id,
+ "",
+ None,
+ step,
+ step + 2,
+ ),
+ channels,
+ managed,
+ task,
+ ),
+ },
+ ),
+ )
+ # save task writes
+ for task_id, task in zip(run_task_ids, run_tasks):
+ # channel writes are saved to current checkpoint
+ channel_writes = [w for w in task.writes if w[0] != PUSH]
+ if saved and channel_writes:
+ await checkpointer.aput_writes(
+ checkpoint_config, channel_writes, task_id
+ )
+ # apply to checkpoint and save
+ apply_writes(
+ checkpoint,
+ channels,
+ run_tasks,
+ checkpointer.get_next_version,
+ self.trigger_to_nodes,
+ )
+ checkpoint = create_checkpoint(checkpoint, channels, step + 1)
+ # save checkpoint, after applying writes
+ next_config = await checkpointer.aput(
+ checkpoint_config,
+ checkpoint,
+ {
+ "source": "update",
+ "step": step + 1,
+ "parents": saved.metadata.get("parents", {}) if saved else {},
+ },
+ get_new_channel_versions(
+ checkpoint_previous_versions, checkpoint["channel_versions"]
+ ),
+ )
+ for task_id, task in zip(run_task_ids, run_tasks):
+ # save push writes
+ if push_writes := [w for w in task.writes if w[0] == PUSH]:
+ await checkpointer.aput_writes(next_config, push_writes, task_id)
+ return patch_checkpoint_map(next_config, saved.metadata if saved else None)
+
+ current_config = patch_configurable(
+ config, {CONFIG_KEY_THREAD_ID: str(config[CONF][CONFIG_KEY_THREAD_ID])}
+ )
+ for superstep in supersteps:
+ current_config = await aperform_superstep(current_config, superstep)
+ return current_config
+
+ def update_state(
+ self,
+ config: RunnableConfig,
+ values: dict[str, Any] | Any | None,
+ as_node: str | None = None,
+ task_id: str | None = None,
+ ) -> RunnableConfig:
+ """Update the state of the graph with the given values, as if they came from
+ node `as_node`. If `as_node` is not provided, it will be set to the last node
+ that updated the state, if not ambiguous.
+ """
+ return self.bulk_update_state(config, [[StateUpdate(values, as_node, task_id)]])
+
+ async def aupdate_state(
+ self,
+ config: RunnableConfig,
+ values: dict[str, Any] | Any,
+ as_node: str | None = None,
+ task_id: str | None = None,
+ ) -> RunnableConfig:
+ """Asynchronously update the state of the graph with the given values, as if they came from
+ node `as_node`. If `as_node` is not provided, it will be set to the last node
+ that updated the state, if not ambiguous.
+ """
+ return await self.abulk_update_state(
+ config, [[StateUpdate(values, as_node, task_id)]]
+ )
+
+ def _defaults(
+ self,
+ config: RunnableConfig,
+ *,
+ stream_mode: StreamMode | Sequence[StreamMode],
+ print_mode: StreamMode | Sequence[StreamMode],
+ output_keys: str | Sequence[str] | None,
+ interrupt_before: All | Sequence[str] | None,
+ interrupt_after: All | Sequence[str] | None,
+ durability: Durability | None = None,
+ ) -> tuple[
+ set[StreamMode],
+ str | Sequence[str],
+ All | Sequence[str],
+ All | Sequence[str],
+ BaseCheckpointSaver | None,
+ BaseStore | None,
+ BaseCache | None,
+ Durability,
+ ]:
+ if config["recursion_limit"] < 1:
+ raise ValueError("recursion_limit must be at least 1")
+ if output_keys is None:
+ output_keys = self.stream_channels_asis
+ else:
+ validate_keys(output_keys, self.channels)
+ interrupt_before = interrupt_before or self.interrupt_before_nodes
+ interrupt_after = interrupt_after or self.interrupt_after_nodes
+ if isinstance(stream_mode, str):
+ stream_modes = {stream_mode}
+ else:
+ stream_modes = set(stream_mode)
+ if isinstance(print_mode, str):
+ stream_modes.add(print_mode)
+ else:
+ stream_modes.update(print_mode)
+ if self.checkpointer is False:
+ checkpointer: BaseCheckpointSaver | None = None
+ elif CONFIG_KEY_CHECKPOINTER in config.get(CONF, {}):
+ checkpointer = config[CONF][CONFIG_KEY_CHECKPOINTER]
+ elif self.checkpointer is True:
+ raise RuntimeError("checkpointer=True cannot be used for root graphs.")
+ else:
+ checkpointer = self.checkpointer
+ if checkpointer and not config.get(CONF):
+ raise ValueError(
+ "Checkpointer requires one or more of the following 'configurable' "
+ "keys: thread_id, checkpoint_ns, checkpoint_id"
+ )
+ if CONFIG_KEY_RUNTIME in config.get(CONF, {}):
+ store: BaseStore | None = config[CONF][CONFIG_KEY_RUNTIME].store
+ else:
+ store = self.store
+ if CONFIG_KEY_CACHE in config.get(CONF, {}):
+ cache: BaseCache | None = config[CONF][CONFIG_KEY_CACHE]
+ else:
+ cache = self.cache
+ if durability is None:
+ durability = config.get(CONF, {}).get(CONFIG_KEY_DURABILITY, "async")
+ return (
+ stream_modes,
+ output_keys,
+ interrupt_before,
+ interrupt_after,
+ checkpointer,
+ store,
+ cache,
+ durability,
+ )
+
+ def stream(
+ self,
+ input: InputT | Command | None,
+ config: RunnableConfig | None = None,
+ *,
+ context: ContextT | None = None,
+ stream_mode: StreamMode | Sequence[StreamMode] | None = None,
+ print_mode: StreamMode | Sequence[StreamMode] = (),
+ output_keys: str | Sequence[str] | None = None,
+ interrupt_before: All | Sequence[str] | None = None,
+ interrupt_after: All | Sequence[str] | None = None,
+ durability: Durability | None = None,
+ subgraphs: bool = False,
+ debug: bool | None = None,
+ **kwargs: Unpack[DeprecatedKwargs],
+ ) -> Iterator[dict[str, Any] | Any]:
+ """Stream graph steps for a single input.
+
+ Args:
+ input: The input to the graph.
+ config: The configuration to use for the run.
+ context: The static context to use for the run.
+ !!! version-added "Added in version 0.6.0"
+ stream_mode: The mode to stream output, defaults to `self.stream_mode`.
+
+ Options are:
+
+ - `"values"`: Emit all values in the state after each step, including interrupts.
+ When used with functional API, values are emitted once at the end of the workflow.
+ - `"updates"`: Emit only the node or task names and updates returned by the nodes or tasks after each step.
+ If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are emitted separately.
+ - `"custom"`: Emit custom data from inside nodes or tasks using `StreamWriter`.
+ - `"messages"`: Emit LLM messages token-by-token together with metadata for any LLM invocations inside nodes or tasks.
+ - Will be emitted as 2-tuples `(LLM token, metadata)`.
+ - `"checkpoints"`: Emit an event when a checkpoint is created, in the same format as returned by `get_state()`.
+ - `"tasks"`: Emit events when tasks start and finish, including their results and errors.
+ - `"debug"`: Emit debug events with as much information as possible for each step.
+
+ You can pass a list as the `stream_mode` parameter to stream multiple modes at once.
+ The streamed outputs will be tuples of `(mode, data)`.
+
+ See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
+ print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes.
+
+ Does not affect the output of the graph in any way.
+ output_keys: The keys to stream, defaults to all non-context channels.
+ interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph.
+ interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph.
+ durability: The durability mode for the graph execution, defaults to `"async"`.
+
+ Options are:
+
+ - `"sync"`: Changes are persisted synchronously before the next step starts.
+ - `"async"`: Changes are persisted asynchronously while the next step executes.
+ - `"exit"`: Changes are persisted only when the graph exits.
+ subgraphs: Whether to stream events from inside subgraphs, defaults to `False`.
+
+ If `True`, the events will be emitted as tuples `(namespace, data)`,
+ or `(namespace, mode, data)` if `stream_mode` is a list,
+ where `namespace` is a tuple with the path to the node where a subgraph is invoked,
+ e.g. `("parent_node:", "child_node:")`.
+
+ See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
+
+ Yields:
+ The output of each step in the graph. The output shape depends on the `stream_mode`.
+ """
+ if (checkpoint_during := kwargs.get("checkpoint_during")) is not None:
+ warnings.warn(
+ "`checkpoint_during` is deprecated and will be removed. Please use `durability` instead.",
+ category=LangGraphDeprecatedSinceV10,
+ stacklevel=2,
+ )
+ if durability is not None:
+ raise ValueError(
+ "Cannot use both `checkpoint_during` and `durability` parameters. Please use `durability` instead."
+ )
+ durability = "async" if checkpoint_during else "exit"
+
+ if stream_mode is None:
+ # if being called as a node in another graph, default to values mode
+ # but don't overwrite stream_mode arg if provided
+ stream_mode = (
+ "values"
+ if config is not None and CONFIG_KEY_TASK_ID in config.get(CONF, {})
+ else self.stream_mode
+ )
+ if debug or self.debug:
+ print_mode = ["updates", "values"]
+
+ stream = SyncQueue()
+
+ config = ensure_config(self.config, config)
+ callback_manager = get_callback_manager_for_config(config)
+ run_manager = callback_manager.on_chain_start(
+ None,
+ input,
+ name=config.get("run_name", self.get_name()),
+ run_id=config.get("run_id"),
+ )
+ try:
+ # assign defaults
+ (
+ stream_modes,
+ output_keys,
+ interrupt_before_,
+ interrupt_after_,
+ checkpointer,
+ store,
+ cache,
+ durability_,
+ ) = self._defaults(
+ config,
+ stream_mode=stream_mode,
+ print_mode=print_mode,
+ output_keys=output_keys,
+ interrupt_before=interrupt_before,
+ interrupt_after=interrupt_after,
+ durability=durability,
+ )
+ if checkpointer is None and durability is not None:
+ warnings.warn(
+ "`durability` has no effect when no checkpointer is present.",
+ )
+ # set up subgraph checkpointing
+ if self.checkpointer is True:
+ ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS])
+ config[CONF][CONFIG_KEY_CHECKPOINT_NS] = recast_checkpoint_ns(ns)
+ # set up messages stream mode
+ if "messages" in stream_modes:
+ ns_ = cast(str | None, config[CONF].get(CONFIG_KEY_CHECKPOINT_NS))
+ run_manager.inheritable_handlers.append(
+ StreamMessagesHandler(
+ stream.put,
+ subgraphs,
+ parent_ns=tuple(ns_.split(NS_SEP)) if ns_ else None,
+ )
+ )
+
+ # set up custom stream mode
+ if "custom" in stream_modes:
+
+ def stream_writer(c: Any) -> None:
+ stream.put(
+ (
+ tuple(
+ get_config()[CONF][CONFIG_KEY_CHECKPOINT_NS].split(
+ NS_SEP
+ )[:-1]
+ ),
+ "custom",
+ c,
+ )
+ )
+ elif CONFIG_KEY_STREAM in config[CONF]:
+ stream_writer = config[CONF][CONFIG_KEY_RUNTIME].stream_writer
+ else:
+
+ def stream_writer(c: Any) -> None:
+ pass
+
+ # set durability mode for subgraphs
+ if durability is not None:
+ config[CONF][CONFIG_KEY_DURABILITY] = durability_
+
+ runtime = Runtime(
+ context=_coerce_context(self.context_schema, context),
+ store=store,
+ stream_writer=stream_writer,
+ previous=None,
+ )
+ parent_runtime = config[CONF].get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME)
+ runtime = parent_runtime.merge(runtime)
+ config[CONF][CONFIG_KEY_RUNTIME] = runtime
+
+ with SyncPregelLoop(
+ input,
+ stream=StreamProtocol(stream.put, stream_modes),
+ config=config,
+ store=store,
+ cache=cache,
+ checkpointer=checkpointer,
+ nodes=self.nodes,
+ specs=self.channels,
+ output_keys=output_keys,
+ input_keys=self.input_channels,
+ stream_keys=self.stream_channels_asis,
+ interrupt_before=interrupt_before_,
+ interrupt_after=interrupt_after_,
+ manager=run_manager,
+ durability=durability_,
+ trigger_to_nodes=self.trigger_to_nodes,
+ migrate_checkpoint=self._migrate_checkpoint,
+ retry_policy=self.retry_policy,
+ cache_policy=self.cache_policy,
+ ) as loop:
+ # create runner
+ runner = PregelRunner(
+ submit=config[CONF].get(
+ CONFIG_KEY_RUNNER_SUBMIT, weakref.WeakMethod(loop.submit)
+ ),
+ put_writes=weakref.WeakMethod(loop.put_writes),
+ node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
+ )
+ # enable subgraph streaming
+ if subgraphs:
+ loop.config[CONF][CONFIG_KEY_STREAM] = loop.stream
+ # enable concurrent streaming
+ get_waiter: Callable[[], concurrent.futures.Future[None]] | None = None
+ if (
+ self.stream_eager
+ or subgraphs
+ or "messages" in stream_modes
+ or "custom" in stream_modes
+ ):
+ # we are careful to have a single waiter live at any one time
+ # because on exit we increment semaphore count by exactly 1
+ waiter: concurrent.futures.Future | None = None
+ # because sync futures cannot be cancelled, we instead
+ # release the stream semaphore on exit, which will cause
+ # a pending waiter to return immediately
+ loop.stack.callback(stream._count.release)
+
+ def get_waiter() -> concurrent.futures.Future[None]:
+ nonlocal waiter
+ if waiter is None or waiter.done():
+ waiter = loop.submit(stream.wait)
+ return waiter
+ else:
+ return waiter
+
+ # Similarly to Bulk Synchronous Parallel / Pregel model
+ # computation proceeds in steps, while there are channel updates.
+ # Channel updates from step N are only visible in step N+1
+ # channels are guaranteed to be immutable for the duration of the step,
+ # with channel updates applied only at the transition between steps.
+ while loop.tick():
+ for task in loop.match_cached_writes():
+ loop.output_writes(task.id, task.writes, cached=True)
+ for _ in runner.tick(
+ [t for t in loop.tasks.values() if not t.writes],
+ timeout=self.step_timeout,
+ get_waiter=get_waiter,
+ schedule_task=loop.accept_push,
+ ):
+ # emit output
+ yield from _output(
+ stream_mode, print_mode, subgraphs, stream.get, queue.Empty
+ )
+ loop.after_tick()
+ # wait for checkpoint
+ if durability_ == "sync":
+ loop._put_checkpoint_fut.result()
+ # emit output
+ yield from _output(
+ stream_mode, print_mode, subgraphs, stream.get, queue.Empty
+ )
+ # handle exit
+ if loop.status == "out_of_steps":
+ msg = create_error_message(
+ message=(
+ f"Recursion limit of {config['recursion_limit']} reached "
+ "without hitting a stop condition. You can increase the "
+ "limit by setting the `recursion_limit` config key."
+ ),
+ error_code=ErrorCode.GRAPH_RECURSION_LIMIT,
+ )
+ raise GraphRecursionError(msg)
+ # set final channel values as run output
+ run_manager.on_chain_end(loop.output)
+ except BaseException as e:
+ run_manager.on_chain_error(e)
+ raise
+
+ async def astream(
+ self,
+ input: InputT | Command | None,
+ config: RunnableConfig | None = None,
+ *,
+ context: ContextT | None = None,
+ stream_mode: StreamMode | Sequence[StreamMode] | None = None,
+ print_mode: StreamMode | Sequence[StreamMode] = (),
+ output_keys: str | Sequence[str] | None = None,
+ interrupt_before: All | Sequence[str] | None = None,
+ interrupt_after: All | Sequence[str] | None = None,
+ durability: Durability | None = None,
+ subgraphs: bool = False,
+ debug: bool | None = None,
+ **kwargs: Unpack[DeprecatedKwargs],
+ ) -> AsyncIterator[dict[str, Any] | Any]:
+ """Asynchronously stream graph steps for a single input.
+
+ Args:
+ input: The input to the graph.
+ config: The configuration to use for the run.
+ context: The static context to use for the run.
+ !!! version-added "Added in version 0.6.0"
+ stream_mode: The mode to stream output, defaults to `self.stream_mode`.
+
+ Options are:
+
+ - `"values"`: Emit all values in the state after each step, including interrupts.
+ When used with functional API, values are emitted once at the end of the workflow.
+ - `"updates"`: Emit only the node or task names and updates returned by the nodes or tasks after each step.
+ If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are emitted separately.
+ - `"custom"`: Emit custom data from inside nodes or tasks using `StreamWriter`.
+ - `"messages"`: Emit LLM messages token-by-token together with metadata for any LLM invocations inside nodes or tasks.
+ - Will be emitted as 2-tuples `(LLM token, metadata)`.
+ - `"checkpoints"`: Emit an event when a checkpoint is created, in the same format as returned by `get_state()`.
+ - `"tasks"`: Emit events when tasks start and finish, including their results and errors.
+ - `"debug"`: Emit debug events with as much information as possible for each step.
+
+ You can pass a list as the `stream_mode` parameter to stream multiple modes at once.
+ The streamed outputs will be tuples of `(mode, data)`.
+
+ See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
+ print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes.
+
+ Does not affect the output of the graph in any way.
+ output_keys: The keys to stream, defaults to all non-context channels.
+ interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph.
+ interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph.
+ durability: The durability mode for the graph execution, defaults to `"async"`.
+
+ Options are:
+
+ - `"sync"`: Changes are persisted synchronously before the next step starts.
+ - `"async"`: Changes are persisted asynchronously while the next step executes.
+ - `"exit"`: Changes are persisted only when the graph exits.
+ subgraphs: Whether to stream events from inside subgraphs, defaults to `False`.
+
+ If `True`, the events will be emitted as tuples `(namespace, data)`,
+ or `(namespace, mode, data)` if `stream_mode` is a list,
+ where `namespace` is a tuple with the path to the node where a subgraph is invoked,
+ e.g. `("parent_node:", "child_node:")`.
+
+ See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
+
+ Yields:
+ The output of each step in the graph. The output shape depends on the `stream_mode`.
+ """
+ if (checkpoint_during := kwargs.get("checkpoint_during")) is not None:
+ warnings.warn(
+ "`checkpoint_during` is deprecated and will be removed. Please use `durability` instead.",
+ category=LangGraphDeprecatedSinceV10,
+ stacklevel=2,
+ )
+ if durability is not None:
+ raise ValueError(
+ "Cannot use both `checkpoint_during` and `durability` parameters. Please use `durability` instead."
+ )
+ durability = "async" if checkpoint_during else "exit"
+
+ if stream_mode is None:
+ # if being called as a node in another graph, default to values mode
+ # but don't overwrite stream_mode arg if provided
+ stream_mode = (
+ "values"
+ if config is not None and CONFIG_KEY_TASK_ID in config.get(CONF, {})
+ else self.stream_mode
+ )
+ if debug or self.debug:
+ print_mode = ["updates", "values"]
+
+ stream = AsyncQueue()
+ aioloop = asyncio.get_running_loop()
+ stream_put = cast(
+ Callable[[StreamChunk], None],
+ partial(aioloop.call_soon_threadsafe, stream.put_nowait),
+ )
+
+ config = ensure_config(self.config, config)
+ callback_manager = get_async_callback_manager_for_config(config)
+ run_manager = await callback_manager.on_chain_start(
+ None,
+ input,
+ name=config.get("run_name", self.get_name()),
+ run_id=config.get("run_id"),
+ )
+ # if running from astream_log() run each proc with streaming
+ do_stream = (
+ next(
+ (
+ True
+ for h in run_manager.handlers
+ if isinstance(h, _StreamingCallbackHandler)
+ and not isinstance(h, StreamMessagesHandler)
+ ),
+ False,
+ )
+ if _StreamingCallbackHandler is not None
+ else False
+ )
+ try:
+ # assign defaults
+ (
+ stream_modes,
+ output_keys,
+ interrupt_before_,
+ interrupt_after_,
+ checkpointer,
+ store,
+ cache,
+ durability_,
+ ) = self._defaults(
+ config,
+ stream_mode=stream_mode,
+ print_mode=print_mode,
+ output_keys=output_keys,
+ interrupt_before=interrupt_before,
+ interrupt_after=interrupt_after,
+ durability=durability,
+ )
+ if checkpointer is None and durability is not None:
+ warnings.warn(
+ "`durability` has no effect when no checkpointer is present.",
+ )
+ # set up subgraph checkpointing
+ if self.checkpointer is True:
+ ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS])
+ config[CONF][CONFIG_KEY_CHECKPOINT_NS] = recast_checkpoint_ns(ns)
+ # set up messages stream mode
+ if "messages" in stream_modes:
+ # namespace can be None in a root level graph?
+ ns_ = cast(str | None, config[CONF].get(CONFIG_KEY_CHECKPOINT_NS))
+ run_manager.inheritable_handlers.append(
+ StreamMessagesHandler(
+ stream_put,
+ subgraphs,
+ parent_ns=tuple(ns_.split(NS_SEP)) if ns_ else None,
+ )
+ )
+
+ # set up custom stream mode
+ def stream_writer(c: Any) -> None:
+ aioloop.call_soon_threadsafe(
+ stream.put_nowait,
+ (
+ tuple(
+ get_config()[CONF][CONFIG_KEY_CHECKPOINT_NS].split(NS_SEP)[
+ :-1
+ ]
+ ),
+ "custom",
+ c,
+ ),
+ )
+
+ if "custom" in stream_modes:
+
+ def stream_writer(c: Any) -> None:
+ aioloop.call_soon_threadsafe(
+ stream.put_nowait,
+ (
+ tuple(
+ get_config()[CONF][CONFIG_KEY_CHECKPOINT_NS].split(
+ NS_SEP
+ )[:-1]
+ ),
+ "custom",
+ c,
+ ),
+ )
+ elif CONFIG_KEY_STREAM in config[CONF]:
+ stream_writer = config[CONF][CONFIG_KEY_RUNTIME].stream_writer
+ else:
+
+ def stream_writer(c: Any) -> None:
+ pass
+
+ # set durability mode for subgraphs
+ if durability is not None:
+ config[CONF][CONFIG_KEY_DURABILITY] = durability_
+
+ runtime = Runtime(
+ context=_coerce_context(self.context_schema, context),
+ store=store,
+ stream_writer=stream_writer,
+ previous=None,
+ )
+ parent_runtime = config[CONF].get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME)
+ runtime = parent_runtime.merge(runtime)
+ config[CONF][CONFIG_KEY_RUNTIME] = runtime
+
+ async with AsyncPregelLoop(
+ input,
+ stream=StreamProtocol(stream.put_nowait, stream_modes),
+ config=config,
+ store=store,
+ cache=cache,
+ checkpointer=checkpointer,
+ nodes=self.nodes,
+ specs=self.channels,
+ output_keys=output_keys,
+ input_keys=self.input_channels,
+ stream_keys=self.stream_channels_asis,
+ interrupt_before=interrupt_before_,
+ interrupt_after=interrupt_after_,
+ manager=run_manager,
+ durability=durability_,
+ trigger_to_nodes=self.trigger_to_nodes,
+ migrate_checkpoint=self._migrate_checkpoint,
+ retry_policy=self.retry_policy,
+ cache_policy=self.cache_policy,
+ ) as loop:
+ # create runner
+ runner = PregelRunner(
+ submit=config[CONF].get(
+ CONFIG_KEY_RUNNER_SUBMIT, weakref.WeakMethod(loop.submit)
+ ),
+ put_writes=weakref.WeakMethod(loop.put_writes),
+ use_astream=do_stream,
+ node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
+ )
+ # enable subgraph streaming
+ if subgraphs:
+ loop.config[CONF][CONFIG_KEY_STREAM] = StreamProtocol(
+ stream_put, stream_modes
+ )
+ # enable concurrent streaming
+ get_waiter: Callable[[], asyncio.Task[None]] | None = None
+ _cleanup_waiter: Callable[[], Awaitable[None]] | None = None
+ if (
+ self.stream_eager
+ or subgraphs
+ or "messages" in stream_modes
+ or "custom" in stream_modes
+ ):
+ # Keep a single waiter task alive; ensure cleanup on exit.
+ waiter: asyncio.Task[None] | None = None
+
+ def get_waiter() -> asyncio.Task[None]:
+ nonlocal waiter
+ if waiter is None or waiter.done():
+ waiter = aioloop.create_task(stream.wait())
+
+ def _clear(t: asyncio.Task[None]) -> None:
+ nonlocal waiter
+ if waiter is t:
+ waiter = None
+
+ waiter.add_done_callback(_clear)
+ return waiter
+
+ async def _cleanup_waiter() -> None:
+ """Wake pending waiter and/or cancel+await to avoid pending tasks."""
+ nonlocal waiter
+ # Try to wake via semaphore like SyncPregelLoop
+ with contextlib.suppress(Exception):
+ if hasattr(stream, "_count"):
+ stream._count.release()
+ t = waiter
+ waiter = None
+ if t is not None and not t.done():
+ t.cancel()
+ with contextlib.suppress(asyncio.CancelledError):
+ await t
+
+ # Similarly to Bulk Synchronous Parallel / Pregel model
+ # computation proceeds in steps, while there are channel updates
+ # channel updates from step N are only visible in step N+1
+ # channels are guaranteed to be immutable for the duration of the step,
+ # with channel updates applied only at the transition between steps
+ try:
+ while loop.tick():
+ for task in await loop.amatch_cached_writes():
+ loop.output_writes(task.id, task.writes, cached=True)
+ async for _ in runner.atick(
+ [t for t in loop.tasks.values() if not t.writes],
+ timeout=self.step_timeout,
+ get_waiter=get_waiter,
+ schedule_task=loop.aaccept_push,
+ ):
+ # emit output
+ for o in _output(
+ stream_mode,
+ print_mode,
+ subgraphs,
+ stream.get_nowait,
+ asyncio.QueueEmpty,
+ ):
+ yield o
+ loop.after_tick()
+ # wait for checkpoint
+ if durability_ == "sync":
+ await cast(asyncio.Future, loop._put_checkpoint_fut)
+ finally:
+ # ensure waiter doesn't remain pending on cancel/shutdown
+ if _cleanup_waiter is not None:
+ await _cleanup_waiter()
+
+ # emit output
+ for o in _output(
+ stream_mode,
+ print_mode,
+ subgraphs,
+ stream.get_nowait,
+ asyncio.QueueEmpty,
+ ):
+ yield o
+ # handle exit
+ if loop.status == "out_of_steps":
+ msg = create_error_message(
+ message=(
+ f"Recursion limit of {config['recursion_limit']} reached "
+ "without hitting a stop condition. You can increase the "
+ "limit by setting the `recursion_limit` config key."
+ ),
+ error_code=ErrorCode.GRAPH_RECURSION_LIMIT,
+ )
+ raise GraphRecursionError(msg)
+ # set final channel values as run output
+ await run_manager.on_chain_end(loop.output)
+ except BaseException as e:
+ await asyncio.shield(run_manager.on_chain_error(e))
+ raise
+
+ def invoke(
+ self,
+ input: InputT | Command | None,
+ config: RunnableConfig | None = None,
+ *,
+ context: ContextT | None = None,
+ stream_mode: StreamMode = "values",
+ print_mode: StreamMode | Sequence[StreamMode] = (),
+ output_keys: str | Sequence[str] | None = None,
+ interrupt_before: All | Sequence[str] | None = None,
+ interrupt_after: All | Sequence[str] | None = None,
+ durability: Durability | None = None,
+ **kwargs: Any,
+ ) -> dict[str, Any] | Any:
+ """Run the graph with a single input and config.
+
+ Args:
+ input: The input data for the graph. It can be a dictionary or any other type.
+ config: The configuration for the graph run.
+ context: The static context to use for the run.
+ !!! version-added "Added in version 0.6.0"
+ stream_mode: The stream mode for the graph run.
+ print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes.
+
+ Does not affect the output of the graph in any way.
+ output_keys: The output keys to retrieve from the graph run.
+ interrupt_before: The nodes to interrupt the graph run before.
+ interrupt_after: The nodes to interrupt the graph run after.
+ durability: The durability mode for the graph execution, defaults to `"async"`.
+
+ Options are:
+
+ - `"sync"`: Changes are persisted synchronously before the next step starts.
+ - `"async"`: Changes are persisted asynchronously while the next step executes.
+ - `"exit"`: Changes are persisted only when the graph exits.
+ **kwargs: Additional keyword arguments to pass to the graph run.
+
+ Returns:
+ The output of the graph run. If `stream_mode` is `"values"`, it returns the latest output.
+ If `stream_mode` is not `"values"`, it returns a list of output chunks.
+ """
+ output_keys = output_keys if output_keys is not None else self.output_channels
+
+ latest: dict[str, Any] | Any = None
+ chunks: list[dict[str, Any] | Any] = []
+ interrupts: list[Interrupt] = []
+
+ for chunk in self.stream(
+ input,
+ config,
+ context=context,
+ stream_mode=["updates", "values"]
+ if stream_mode == "values"
+ else stream_mode,
+ print_mode=print_mode,
+ output_keys=output_keys,
+ interrupt_before=interrupt_before,
+ interrupt_after=interrupt_after,
+ durability=durability,
+ **kwargs,
+ ):
+ if stream_mode == "values":
+ if len(chunk) == 2:
+ mode, payload = cast(tuple[StreamMode, Any], chunk)
+ else:
+ _, mode, payload = cast(
+ tuple[tuple[str, ...], StreamMode, Any], chunk
+ )
+ if (
+ mode == "updates"
+ and isinstance(payload, dict)
+ and (ints := payload.get(INTERRUPT)) is not None
+ ):
+ interrupts.extend(ints)
+ elif mode == "values":
+ latest = payload
+ else:
+ chunks.append(chunk)
+
+ if stream_mode == "values":
+ if interrupts:
+ return (
+ {**latest, INTERRUPT: interrupts}
+ if isinstance(latest, dict)
+ else {INTERRUPT: interrupts}
+ )
+ return latest
+ else:
+ return chunks
+
+ async def ainvoke(
+ self,
+ input: InputT | Command | None,
+ config: RunnableConfig | None = None,
+ *,
+ context: ContextT | None = None,
+ stream_mode: StreamMode = "values",
+ print_mode: StreamMode | Sequence[StreamMode] = (),
+ output_keys: str | Sequence[str] | None = None,
+ interrupt_before: All | Sequence[str] | None = None,
+ interrupt_after: All | Sequence[str] | None = None,
+ durability: Durability | None = None,
+ **kwargs: Any,
+ ) -> dict[str, Any] | Any:
+ """Asynchronously run the graph with a single input and config.
+
+ Args:
+ input: The input data for the graph. It can be a dictionary or any other type.
+ config: The configuration for the graph run.
+ context: The static context to use for the run.
+ !!! version-added "Added in version 0.6.0"
+ stream_mode: The stream mode for the graph run.
+ print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes.
+
+ Does not affect the output of the graph in any way.
+ output_keys: The output keys to retrieve from the graph run.
+ interrupt_before: The nodes to interrupt the graph run before.
+ interrupt_after: The nodes to interrupt the graph run after.
+ durability: The durability mode for the graph execution, defaults to `"async"`.
+
+ Options are:
+
+ - `"sync"`: Changes are persisted synchronously before the next step starts.
+ - `"async"`: Changes are persisted asynchronously while the next step executes.
+ - `"exit"`: Changes are persisted only when the graph exits.
+ **kwargs: Additional keyword arguments to pass to the graph run.
+
+ Returns:
+ The output of the graph run. If `stream_mode` is `"values"`, it returns the latest output.
+ If `stream_mode` is not `"values"`, it returns a list of output chunks.
+ """
+ output_keys = output_keys if output_keys is not None else self.output_channels
+
+ latest: dict[str, Any] | Any = None
+ chunks: list[dict[str, Any] | Any] = []
+ interrupts: list[Interrupt] = []
+
+ async for chunk in self.astream(
+ input,
+ config,
+ context=context,
+ stream_mode=["updates", "values"]
+ if stream_mode == "values"
+ else stream_mode,
+ print_mode=print_mode,
+ output_keys=output_keys,
+ interrupt_before=interrupt_before,
+ interrupt_after=interrupt_after,
+ durability=durability,
+ **kwargs,
+ ):
+ if stream_mode == "values":
+ if len(chunk) == 2:
+ mode, payload = cast(tuple[StreamMode, Any], chunk)
+ else:
+ _, mode, payload = cast(
+ tuple[tuple[str, ...], StreamMode, Any], chunk
+ )
+ if (
+ mode == "updates"
+ and isinstance(payload, dict)
+ and (ints := payload.get(INTERRUPT)) is not None
+ ):
+ interrupts.extend(ints)
+ elif mode == "values":
+ latest = payload
+ else:
+ chunks.append(chunk)
+
+ if stream_mode == "values":
+ if interrupts:
+ return (
+ {**latest, INTERRUPT: interrupts}
+ if isinstance(latest, dict)
+ else {INTERRUPT: interrupts}
+ )
+ return latest
+ else:
+ return chunks
+
+ def clear_cache(self, nodes: Sequence[str] | None = None) -> None:
+ """Clear the cache for the given nodes."""
+ if not self.cache:
+ raise ValueError("No cache is set for this graph. Cannot clear cache.")
+ nodes = nodes or self.nodes.keys()
+ # collect namespaces to clear
+ namespaces: list[tuple[str, ...]] = []
+ for node in nodes:
+ if node in self.nodes:
+ namespaces.append(
+ (
+ CACHE_NS_WRITES,
+ (identifier(self.nodes[node]) or "__dynamic__"),
+ node,
+ ),
+ )
+ # clear cache
+ self.cache.clear(namespaces)
+
+ async def aclear_cache(self, nodes: Sequence[str] | None = None) -> None:
+ """Asynchronously clear the cache for the given nodes."""
+ if not self.cache:
+ raise ValueError("No cache is set for this graph. Cannot clear cache.")
+ nodes = nodes or self.nodes.keys()
+ # collect namespaces to clear
+ namespaces: list[tuple[str, ...]] = []
+ for node in nodes:
+ if node in self.nodes:
+ namespaces.append(
+ (
+ CACHE_NS_WRITES,
+ (identifier(self.nodes[node]) or "__dynamic__"),
+ node,
+ ),
+ )
+ # clear cache
+ await self.cache.aclear(namespaces)
+
+
+def _trigger_to_nodes(nodes: dict[str, PregelNode]) -> Mapping[str, Sequence[str]]:
+ """Index from a trigger to nodes that depend on it."""
+ trigger_to_nodes: defaultdict[str, list[str]] = defaultdict(list)
+ for name, node in nodes.items():
+ for trigger in node.triggers:
+ trigger_to_nodes[trigger].append(name)
+ return dict(trigger_to_nodes)
+
+
+def _output(
+ stream_mode: StreamMode | Sequence[StreamMode],
+ print_mode: StreamMode | Sequence[StreamMode],
+ stream_subgraphs: bool,
+ getter: Callable[[], tuple[tuple[str, ...], str, Any]],
+ empty_exc: type[Exception],
+) -> Iterator:
+ while True:
+ try:
+ ns, mode, payload = getter()
+ except empty_exc:
+ break
+ if mode in print_mode:
+ if stream_subgraphs and ns:
+ print(
+ " ".join(
+ (
+ get_bolded_text(f"[{mode}]"),
+ get_colored_text(f"[graph={ns}]", color="yellow"),
+ repr(payload),
+ )
+ )
+ )
+ else:
+ print(
+ " ".join(
+ (
+ get_bolded_text(f"[{mode}]"),
+ repr(payload),
+ )
+ )
+ )
+ if mode in stream_mode:
+ if stream_subgraphs and isinstance(stream_mode, list):
+ yield (ns, mode, payload)
+ elif isinstance(stream_mode, list):
+ yield (mode, payload)
+ elif stream_subgraphs:
+ yield (ns, payload)
+ else:
+ yield payload
+
+
+def _coerce_context(
+ context_schema: type[ContextT] | None, context: Any
+) -> ContextT | None:
+ """Coerce context input to the appropriate schema type.
+
+ If context is a dict and context_schema is a dataclass or pydantic model, we coerce.
+ Else, we return the context as-is.
+
+ Args:
+ context_schema: The schema type to coerce to (BaseModel, dataclass, or TypedDict)
+ context: The context value to coerce
+
+ Returns:
+ The coerced context value or None if context is None
+ """
+ if context is None:
+ return None
+
+ if context_schema is None:
+ return context
+
+ schema_is_class = issubclass(context_schema, BaseModel) or is_dataclass(
+ context_schema
+ )
+ if isinstance(context, dict) and schema_is_class:
+ return context_schema(**context) # type: ignore[misc]
+
+ return cast(ContextT, context)
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/protocol.py b/langgraph/source/libs/langgraph/langgraph/pregel/protocol.py
new file mode 100644
index 0000000000000000000000000000000000000000..c9bf6e5ff4c4d965e2ab71a0647a4437b599c87d
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/protocol.py
@@ -0,0 +1,164 @@
+from __future__ import annotations
+
+from abc import abstractmethod
+from collections.abc import AsyncIterator, Callable, Iterator, Sequence
+from typing import Any, Generic, cast
+
+from langchain_core.runnables import Runnable, RunnableConfig
+from langchain_core.runnables.graph import Graph as DrawableGraph
+from typing_extensions import Self
+
+from langgraph.types import All, Command, StateSnapshot, StateUpdate, StreamMode
+from langgraph.typing import ContextT, InputT, OutputT, StateT
+
+__all__ = ("PregelProtocol", "StreamProtocol")
+
+
+class PregelProtocol(Runnable[InputT, Any], Generic[StateT, ContextT, InputT, OutputT]):
+ @abstractmethod
+ def with_config(
+ self, config: RunnableConfig | None = None, **kwargs: Any
+ ) -> Self: ...
+
+ @abstractmethod
+ def get_graph(
+ self,
+ config: RunnableConfig | None = None,
+ *,
+ xray: int | bool = False,
+ ) -> DrawableGraph: ...
+
+ @abstractmethod
+ async def aget_graph(
+ self,
+ config: RunnableConfig | None = None,
+ *,
+ xray: int | bool = False,
+ ) -> DrawableGraph: ...
+
+ @abstractmethod
+ def get_state(
+ self, config: RunnableConfig, *, subgraphs: bool = False
+ ) -> StateSnapshot: ...
+
+ @abstractmethod
+ async def aget_state(
+ self, config: RunnableConfig, *, subgraphs: bool = False
+ ) -> StateSnapshot: ...
+
+ @abstractmethod
+ def get_state_history(
+ self,
+ config: RunnableConfig,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> Iterator[StateSnapshot]: ...
+
+ @abstractmethod
+ def aget_state_history(
+ self,
+ config: RunnableConfig,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ ) -> AsyncIterator[StateSnapshot]: ...
+
+ @abstractmethod
+ def bulk_update_state(
+ self,
+ config: RunnableConfig,
+ updates: Sequence[Sequence[StateUpdate]],
+ ) -> RunnableConfig: ...
+
+ @abstractmethod
+ async def abulk_update_state(
+ self,
+ config: RunnableConfig,
+ updates: Sequence[Sequence[StateUpdate]],
+ ) -> RunnableConfig: ...
+
+ @abstractmethod
+ def update_state(
+ self,
+ config: RunnableConfig,
+ values: dict[str, Any] | Any | None,
+ as_node: str | None = None,
+ ) -> RunnableConfig: ...
+
+ @abstractmethod
+ async def aupdate_state(
+ self,
+ config: RunnableConfig,
+ values: dict[str, Any] | Any | None,
+ as_node: str | None = None,
+ ) -> RunnableConfig: ...
+
+ @abstractmethod
+ def stream(
+ self,
+ input: InputT | Command | None,
+ config: RunnableConfig | None = None,
+ *,
+ context: ContextT | None = None,
+ stream_mode: StreamMode | list[StreamMode] | None = None,
+ interrupt_before: All | Sequence[str] | None = None,
+ interrupt_after: All | Sequence[str] | None = None,
+ subgraphs: bool = False,
+ ) -> Iterator[dict[str, Any] | Any]: ...
+
+ @abstractmethod
+ def astream(
+ self,
+ input: InputT | Command | None,
+ config: RunnableConfig | None = None,
+ *,
+ context: ContextT | None = None,
+ stream_mode: StreamMode | list[StreamMode] | None = None,
+ interrupt_before: All | Sequence[str] | None = None,
+ interrupt_after: All | Sequence[str] | None = None,
+ subgraphs: bool = False,
+ ) -> AsyncIterator[dict[str, Any] | Any]: ...
+
+ @abstractmethod
+ def invoke(
+ self,
+ input: InputT | Command | None,
+ config: RunnableConfig | None = None,
+ *,
+ context: ContextT | None = None,
+ interrupt_before: All | Sequence[str] | None = None,
+ interrupt_after: All | Sequence[str] | None = None,
+ ) -> dict[str, Any] | Any: ...
+
+ @abstractmethod
+ async def ainvoke(
+ self,
+ input: InputT | Command | None,
+ config: RunnableConfig | None = None,
+ *,
+ context: ContextT | None = None,
+ interrupt_before: All | Sequence[str] | None = None,
+ interrupt_after: All | Sequence[str] | None = None,
+ ) -> dict[str, Any] | Any: ...
+
+
+StreamChunk = tuple[tuple[str, ...], str, Any]
+
+
+class StreamProtocol:
+ __slots__ = ("modes", "__call__")
+
+ modes: set[StreamMode]
+
+ __call__: Callable[[Self, StreamChunk], None]
+
+ def __init__(
+ self,
+ __call__: Callable[[StreamChunk], None],
+ modes: set[StreamMode],
+ ) -> None:
+ self.__call__ = cast(Callable[[Self, StreamChunk], None], __call__)
+ self.modes = modes
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/remote.py b/langgraph/source/libs/langgraph/langgraph/pregel/remote.py
new file mode 100644
index 0000000000000000000000000000000000000000..2535d966aeacd523b38284036cb10a24636e9a08
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/remote.py
@@ -0,0 +1,1015 @@
+from __future__ import annotations
+
+import logging
+from collections.abc import AsyncIterator, Iterator, Sequence
+from dataclasses import asdict
+from typing import (
+ Any,
+ Literal,
+ cast,
+)
+from uuid import UUID
+
+import langsmith as ls
+from langchain_core.runnables import RunnableConfig
+from langchain_core.runnables.graph import (
+ Edge as DrawableEdge,
+)
+from langchain_core.runnables.graph import (
+ Graph as DrawableGraph,
+)
+from langchain_core.runnables.graph import (
+ Node as DrawableNode,
+)
+from langgraph.checkpoint.base import CheckpointMetadata
+from langgraph_sdk.client import (
+ LangGraphClient,
+ SyncLangGraphClient,
+ get_client,
+ get_sync_client,
+)
+from langgraph_sdk.schema import (
+ Checkpoint,
+ QueryParamTypes,
+ ThreadState,
+)
+from langgraph_sdk.schema import (
+ Command as CommandSDK,
+)
+from langgraph_sdk.schema import (
+ StreamMode as StreamModeSDK,
+)
+from typing_extensions import Self
+
+from langgraph._internal._config import merge_configs
+from langgraph._internal._constants import (
+ CONF,
+ CONFIG_KEY_CHECKPOINT_ID,
+ CONFIG_KEY_CHECKPOINT_MAP,
+ CONFIG_KEY_CHECKPOINT_NS,
+ CONFIG_KEY_STREAM,
+ CONFIG_KEY_TASK_ID,
+ INTERRUPT,
+ NS_SEP,
+)
+from langgraph.errors import GraphInterrupt, ParentCommand
+from langgraph.pregel.protocol import PregelProtocol, StreamProtocol
+from langgraph.types import (
+ All,
+ Command,
+ Interrupt,
+ PregelTask,
+ StateSnapshot,
+ StreamMode,
+)
+
+logger = logging.getLogger(__name__)
+
+__all__ = ("RemoteGraph", "RemoteException")
+
+_CONF_DROPLIST = frozenset(
+ (
+ CONFIG_KEY_CHECKPOINT_MAP,
+ CONFIG_KEY_CHECKPOINT_ID,
+ CONFIG_KEY_CHECKPOINT_NS,
+ CONFIG_KEY_TASK_ID,
+ ),
+)
+
+
+def _sanitize_config_value(v: Any) -> Any:
+ """Recursively sanitize a config value to ensure it contains only primitives."""
+ if isinstance(v, (str, int, float, bool, UUID)):
+ return v
+ elif isinstance(v, dict):
+ sanitized_dict = {}
+ for k, val in v.items():
+ if isinstance(k, str):
+ sanitized_value = _sanitize_config_value(val)
+ if sanitized_value is not None:
+ sanitized_dict[k] = sanitized_value
+ return sanitized_dict
+ elif isinstance(v, (list, tuple)):
+ sanitized_list = []
+ for item in v:
+ sanitized_item = _sanitize_config_value(item)
+ if sanitized_item is not None:
+ sanitized_list.append(sanitized_item)
+ return sanitized_list
+ return None
+
+
+class RemoteException(Exception):
+ """Exception raised when an error occurs in the remote graph."""
+
+ pass
+
+
+class RemoteGraph(PregelProtocol):
+ """The `RemoteGraph` class is a client implementation for calling remote
+ APIs that implement the LangGraph Server API specification.
+
+ For example, the `RemoteGraph` class can be used to call APIs from deployments
+ on LangSmith Deployment.
+
+ `RemoteGraph` behaves the same way as a `Graph` and can be used directly as
+ a node in another `Graph`.
+ """
+
+ assistant_id: str
+ name: str | None
+
+ def __init__(
+ self,
+ assistant_id: str, # graph_id
+ /,
+ *,
+ url: str | None = None,
+ api_key: str | None = None,
+ headers: dict[str, str] | None = None,
+ client: LangGraphClient | None = None,
+ sync_client: SyncLangGraphClient | None = None,
+ config: RunnableConfig | None = None,
+ name: str | None = None,
+ distributed_tracing: bool = False,
+ ):
+ """Specify `url`, `api_key`, and/or `headers` to create default sync and async clients.
+
+ If `client` or `sync_client` are provided, they will be used instead of the default clients.
+ See `LangGraphClient` and `SyncLangGraphClient` for details on the default clients. At least
+ one of `url`, `client`, or `sync_client` must be provided.
+
+ Args:
+ assistant_id: The assistant ID or graph name of the remote graph to use.
+ url: The URL of the remote API.
+ api_key: The API key to use for authentication. If not provided, it will be read from the environment (`LANGGRAPH_API_KEY`, `LANGSMITH_API_KEY`, or `LANGCHAIN_API_KEY`).
+ headers: Additional headers to include in the requests.
+ client: A `LangGraphClient` instance to use instead of creating a default client.
+ sync_client: A `SyncLangGraphClient` instance to use instead of creating a default client.
+ config: An optional `RunnableConfig` instance with additional configuration.
+ name: Human-readable name to attach to the RemoteGraph instance.
+ This is useful for adding `RemoteGraph` as a subgraph via `graph.add_node(remote_graph)`.
+ If not provided, defaults to the assistant ID.
+ distributed_tracing: Whether to enable sending LangSmith distributed tracing headers.
+ """
+ self.assistant_id = assistant_id
+ if name is None:
+ self.name = assistant_id
+ else:
+ self.name = name
+ self.config = config
+ self.distributed_tracing = distributed_tracing
+
+ if client is None and url is not None:
+ client = get_client(url=url, api_key=api_key, headers=headers)
+ self.client = client
+
+ if sync_client is None and url is not None:
+ sync_client = get_sync_client(url=url, api_key=api_key, headers=headers)
+ self.sync_client = sync_client
+
+ def _validate_client(self) -> LangGraphClient:
+ if self.client is None:
+ raise ValueError(
+ "Async client is not initialized: please provide `url` or `client` when initializing `RemoteGraph`."
+ )
+ return self.client
+
+ def _validate_sync_client(self) -> SyncLangGraphClient:
+ if self.sync_client is None:
+ raise ValueError(
+ "Sync client is not initialized: please provide `url` or `sync_client` when initializing `RemoteGraph`."
+ )
+ return self.sync_client
+
+ def copy(self, update: dict[str, Any]) -> Self:
+ attrs = {**self.__dict__, **update}
+ return self.__class__(attrs.pop("assistant_id"), **attrs)
+
+ def with_config(self, config: RunnableConfig | None = None, **kwargs: Any) -> Self:
+ return self.copy(
+ {"config": merge_configs(self.config, config, cast(RunnableConfig, kwargs))}
+ )
+
+ def _get_drawable_nodes(
+ self, graph: dict[str, list[dict[str, Any]]]
+ ) -> dict[str, DrawableNode]:
+ nodes = {}
+ for node in graph["nodes"]:
+ node_id = str(node["id"])
+ node_data = node.get("data", {})
+
+ # Get node name from node_data if available. If not, use node_id.
+ node_name = node.get("name")
+ if node_name is None:
+ if isinstance(node_data, dict):
+ node_name = node_data.get("name", node_id)
+ else:
+ node_name = node_id
+
+ nodes[node_id] = DrawableNode(
+ id=node_id,
+ name=node_name,
+ data=node_data,
+ metadata=node.get("metadata"),
+ )
+ return nodes
+
+ def get_graph(
+ self,
+ config: RunnableConfig | None = None,
+ *,
+ xray: int | bool = False,
+ headers: dict[str, str] | None = None,
+ params: QueryParamTypes | None = None,
+ ) -> DrawableGraph:
+ """Get graph by graph name.
+
+ This method calls `GET /assistants/{assistant_id}/graph`.
+
+ Args:
+ config: This parameter is not used.
+ xray: Include graph representation of subgraphs. If an integer
+ value is provided, only subgraphs with a depth less than or
+ equal to the value will be included.
+
+ Returns:
+ The graph information for the assistant in JSON format.
+ """
+ sync_client = self._validate_sync_client()
+ graph = sync_client.assistants.get_graph(
+ assistant_id=self.assistant_id,
+ xray=xray,
+ headers=headers,
+ params=params,
+ )
+ return DrawableGraph(
+ nodes=self._get_drawable_nodes(graph),
+ edges=[DrawableEdge(**edge) for edge in graph["edges"]],
+ )
+
+ async def aget_graph(
+ self,
+ config: RunnableConfig | None = None,
+ *,
+ xray: int | bool = False,
+ headers: dict[str, str] | None = None,
+ params: QueryParamTypes | None = None,
+ ) -> DrawableGraph:
+ """Get graph by graph name.
+
+ This method calls `GET /assistants/{assistant_id}/graph`.
+
+ Args:
+ config: This parameter is not used.
+ xray: Include graph representation of subgraphs. If an integer
+ value is provided, only subgraphs with a depth less than or
+ equal to the value will be included.
+
+ Returns:
+ The graph information for the assistant in JSON format.
+ """
+ client = self._validate_client()
+ graph = await client.assistants.get_graph(
+ assistant_id=self.assistant_id,
+ xray=xray,
+ headers=headers,
+ params=params,
+ )
+ return DrawableGraph(
+ nodes=self._get_drawable_nodes(graph),
+ edges=[DrawableEdge(**edge) for edge in graph["edges"]],
+ )
+
+ def _create_state_snapshot(self, state: ThreadState) -> StateSnapshot:
+ tasks: list[PregelTask] = []
+ for task in state["tasks"]:
+ interrupts = tuple(
+ Interrupt(**interrupt) for interrupt in task["interrupts"]
+ )
+
+ tasks.append(
+ PregelTask(
+ id=task["id"],
+ name=task["name"],
+ path=tuple(),
+ error=Exception(task["error"]) if task["error"] else None,
+ interrupts=interrupts,
+ state=(
+ self._create_state_snapshot(task["state"])
+ if task["state"]
+ else (
+ cast(RunnableConfig, {"configurable": task["checkpoint"]})
+ if task["checkpoint"]
+ else None
+ )
+ ),
+ result=task.get("result"),
+ )
+ )
+
+ return StateSnapshot(
+ values=state["values"],
+ next=tuple(state["next"]) if state["next"] else tuple(),
+ config={
+ "configurable": {
+ "thread_id": state["checkpoint"]["thread_id"],
+ "checkpoint_ns": state["checkpoint"]["checkpoint_ns"],
+ "checkpoint_id": state["checkpoint"]["checkpoint_id"],
+ "checkpoint_map": state["checkpoint"].get("checkpoint_map", {}),
+ }
+ },
+ metadata=CheckpointMetadata(**state["metadata"]),
+ created_at=state["created_at"],
+ parent_config=(
+ {
+ "configurable": {
+ "thread_id": state["parent_checkpoint"]["thread_id"],
+ "checkpoint_ns": state["parent_checkpoint"]["checkpoint_ns"],
+ "checkpoint_id": state["parent_checkpoint"]["checkpoint_id"],
+ "checkpoint_map": state["parent_checkpoint"].get(
+ "checkpoint_map", {}
+ ),
+ }
+ }
+ if state["parent_checkpoint"]
+ else None
+ ),
+ tasks=tuple(tasks),
+ interrupts=tuple([i for task in tasks for i in task.interrupts]),
+ )
+
+ def _get_checkpoint(self, config: RunnableConfig | None) -> Checkpoint | None:
+ if config is None:
+ return None
+
+ checkpoint = {}
+
+ if "thread_id" in config["configurable"]:
+ checkpoint["thread_id"] = config["configurable"]["thread_id"]
+ if "checkpoint_ns" in config["configurable"]:
+ checkpoint["checkpoint_ns"] = config["configurable"]["checkpoint_ns"]
+ if "checkpoint_id" in config["configurable"]:
+ checkpoint["checkpoint_id"] = config["configurable"]["checkpoint_id"]
+ if "checkpoint_map" in config["configurable"]:
+ checkpoint["checkpoint_map"] = config["configurable"]["checkpoint_map"]
+
+ return checkpoint if checkpoint else None
+
+ def _get_config(self, checkpoint: Checkpoint) -> RunnableConfig:
+ return {
+ "configurable": {
+ "thread_id": checkpoint["thread_id"],
+ "checkpoint_ns": checkpoint["checkpoint_ns"],
+ "checkpoint_id": checkpoint["checkpoint_id"],
+ "checkpoint_map": checkpoint.get("checkpoint_map", {}),
+ }
+ }
+
+ def _sanitize_config(self, config: RunnableConfig) -> RunnableConfig:
+ """Sanitize the config to remove non-serializable fields."""
+ sanitized: RunnableConfig = {}
+ if "recursion_limit" in config:
+ sanitized["recursion_limit"] = config["recursion_limit"]
+ if "tags" in config:
+ sanitized["tags"] = [tag for tag in config["tags"] if isinstance(tag, str)]
+
+ if "metadata" in config:
+ sanitized["metadata"] = {}
+ for k, v in config["metadata"].items():
+ if (
+ isinstance(k, str)
+ and (sanitized_value := _sanitize_config_value(v)) is not None
+ ):
+ sanitized["metadata"][k] = sanitized_value
+
+ if "configurable" in config:
+ sanitized["configurable"] = {}
+ for k, v in config["configurable"].items():
+ if (
+ isinstance(k, str)
+ and k not in _CONF_DROPLIST
+ and (sanitized_value := _sanitize_config_value(v)) is not None
+ ):
+ sanitized["configurable"][k] = sanitized_value
+
+ return sanitized
+
+ def get_state(
+ self,
+ config: RunnableConfig,
+ *,
+ subgraphs: bool = False,
+ headers: dict[str, str] | None = None,
+ params: QueryParamTypes | None = None,
+ ) -> StateSnapshot:
+ """Get the state of a thread.
+
+ This method calls `POST /threads/{thread_id}/state/checkpoint` if a
+ checkpoint is specified in the config or `GET /threads/{thread_id}/state`
+ if no checkpoint is specified.
+
+ Args:
+ config: A `RunnableConfig` that includes `thread_id` in the
+ `configurable` field.
+ subgraphs: Include subgraphs in the state.
+ headers: Optional custom headers to include with the request.
+ params: Optional query parameters to include with the request.
+
+ Returns:
+ The latest state of the thread.
+ """
+ sync_client = self._validate_sync_client()
+ merged_config = merge_configs(self.config, config)
+
+ state = sync_client.threads.get_state(
+ thread_id=merged_config["configurable"]["thread_id"],
+ checkpoint=self._get_checkpoint(merged_config),
+ subgraphs=subgraphs,
+ headers=headers,
+ params=params,
+ )
+ return self._create_state_snapshot(state)
+
+ async def aget_state(
+ self,
+ config: RunnableConfig,
+ *,
+ subgraphs: bool = False,
+ headers: dict[str, str] | None = None,
+ params: QueryParamTypes | None = None,
+ ) -> StateSnapshot:
+ """Get the state of a thread.
+
+ This method calls `POST /threads/{thread_id}/state/checkpoint` if a
+ checkpoint is specified in the config or `GET /threads/{thread_id}/state`
+ if no checkpoint is specified.
+
+ Args:
+ config: A `RunnableConfig` that includes `thread_id` in the
+ `configurable` field.
+ subgraphs: Include subgraphs in the state.
+ headers: Optional custom headers to include with the request.
+ params: Optional query parameters to include with the request.
+
+ Returns:
+ The latest state of the thread.
+ """
+ client = self._validate_client()
+ merged_config = merge_configs(self.config, config)
+
+ state = await client.threads.get_state(
+ thread_id=merged_config["configurable"]["thread_id"],
+ checkpoint=self._get_checkpoint(merged_config),
+ subgraphs=subgraphs,
+ headers=headers,
+ params=params,
+ )
+ return self._create_state_snapshot(state)
+
+ def get_state_history(
+ self,
+ config: RunnableConfig,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ headers: dict[str, str] | None = None,
+ params: QueryParamTypes | None = None,
+ ) -> Iterator[StateSnapshot]:
+ """Get the state history of a thread.
+
+ This method calls `POST /threads/{thread_id}/history`.
+
+ Args:
+ config: A `RunnableConfig` that includes `thread_id` in the
+ `configurable` field.
+ filter: Metadata to filter on.
+ before: A `RunnableConfig` that includes checkpoint metadata.
+ limit: Max number of states to return.
+
+ Returns:
+ States of the thread.
+ """
+ sync_client = self._validate_sync_client()
+ merged_config = merge_configs(self.config, config)
+
+ states = sync_client.threads.get_history(
+ thread_id=merged_config["configurable"]["thread_id"],
+ limit=limit if limit else 10,
+ before=self._get_checkpoint(before),
+ metadata=filter,
+ checkpoint=self._get_checkpoint(merged_config),
+ headers=headers,
+ params=params,
+ )
+ for state in states:
+ yield self._create_state_snapshot(state)
+
+ async def aget_state_history(
+ self,
+ config: RunnableConfig,
+ *,
+ filter: dict[str, Any] | None = None,
+ before: RunnableConfig | None = None,
+ limit: int | None = None,
+ headers: dict[str, str] | None = None,
+ params: QueryParamTypes | None = None,
+ ) -> AsyncIterator[StateSnapshot]:
+ """Get the state history of a thread.
+
+ This method calls `POST /threads/{thread_id}/history`.
+
+ Args:
+ config: A `RunnableConfig` that includes `thread_id` in the
+ `configurable` field.
+ filter: Metadata to filter on.
+ before: A `RunnableConfig` that includes checkpoint metadata.
+ limit: Max number of states to return.
+ headers: Optional custom headers to include with the request.
+ params: Optional query parameters to include with the request.
+
+ Returns:
+ States of the thread.
+ """
+ client = self._validate_client()
+ merged_config = merge_configs(self.config, config)
+
+ states = await client.threads.get_history(
+ thread_id=merged_config["configurable"]["thread_id"],
+ limit=limit if limit else 10,
+ before=self._get_checkpoint(before),
+ metadata=filter,
+ checkpoint=self._get_checkpoint(merged_config),
+ headers=headers,
+ params=params,
+ )
+ for state in states:
+ yield self._create_state_snapshot(state)
+
+ def bulk_update_state(
+ self,
+ config: RunnableConfig,
+ updates: list[tuple[dict[str, Any] | None, str | None]],
+ ) -> RunnableConfig:
+ raise NotImplementedError
+
+ async def abulk_update_state(
+ self,
+ config: RunnableConfig,
+ updates: list[tuple[dict[str, Any] | None, str | None]],
+ ) -> RunnableConfig:
+ raise NotImplementedError
+
+ def update_state(
+ self,
+ config: RunnableConfig,
+ values: dict[str, Any] | Any | None,
+ as_node: str | None = None,
+ *,
+ headers: dict[str, str] | None = None,
+ params: QueryParamTypes | None = None,
+ ) -> RunnableConfig:
+ """Update the state of a thread.
+
+ This method calls `POST /threads/{thread_id}/state`.
+
+ Args:
+ config: A `RunnableConfig` that includes `thread_id` in the
+ `configurable` field.
+ values: Values to update to the state.
+ as_node: Update the state as if this node had just executed.
+
+ Returns:
+ `RunnableConfig` for the updated thread.
+ """
+ sync_client = self._validate_sync_client()
+ merged_config = merge_configs(self.config, config)
+
+ response: dict = sync_client.threads.update_state( # type: ignore
+ thread_id=merged_config["configurable"]["thread_id"],
+ values=values,
+ as_node=as_node,
+ checkpoint=self._get_checkpoint(merged_config),
+ headers=headers,
+ params=params,
+ )
+ return self._get_config(response["checkpoint"])
+
+ async def aupdate_state(
+ self,
+ config: RunnableConfig,
+ values: dict[str, Any] | Any | None,
+ as_node: str | None = None,
+ *,
+ headers: dict[str, str] | None = None,
+ params: QueryParamTypes | None = None,
+ ) -> RunnableConfig:
+ """Update the state of a thread.
+
+ This method calls `POST /threads/{thread_id}/state`.
+
+ Args:
+ config: A `RunnableConfig` that includes `thread_id` in the
+ `configurable` field.
+ values: Values to update to the state.
+ as_node: Update the state as if this node had just executed.
+
+ Returns:
+ `RunnableConfig` for the updated thread.
+ """
+ client = self._validate_client()
+ merged_config = merge_configs(self.config, config)
+
+ response: dict = await client.threads.update_state( # type: ignore
+ thread_id=merged_config["configurable"]["thread_id"],
+ values=values,
+ as_node=as_node,
+ checkpoint=self._get_checkpoint(merged_config),
+ headers=headers,
+ params=params,
+ )
+ return self._get_config(response["checkpoint"])
+
+ def _get_stream_modes(
+ self,
+ stream_mode: StreamMode | list[StreamMode] | None,
+ config: RunnableConfig | None,
+ default: StreamMode = "updates",
+ ) -> tuple[list[StreamModeSDK], list[StreamModeSDK], bool, StreamProtocol | None]:
+ """Return a tuple of the final list of stream modes sent to the
+ remote graph and a boolean flag indicating if stream mode 'updates'
+ was present in the original list of stream modes.
+
+ 'updates' mode is added to the list of stream modes so that interrupts
+ can be detected in the remote graph.
+ """
+ updated_stream_modes: list[StreamModeSDK] = []
+ req_single = True
+ # coerce to list, or add default stream mode
+ if stream_mode:
+ if isinstance(stream_mode, str):
+ updated_stream_modes.append(stream_mode)
+ else:
+ req_single = False
+ updated_stream_modes.extend(stream_mode)
+ else:
+ updated_stream_modes.append(default)
+ requested_stream_modes = updated_stream_modes.copy()
+ # add any from parent graph
+ stream: StreamProtocol | None = (
+ (config or {}).get(CONF, {}).get(CONFIG_KEY_STREAM)
+ )
+ if stream:
+ updated_stream_modes.extend(stream.modes)
+ # map "messages" to "messages-tuple"
+ if "messages" in updated_stream_modes:
+ updated_stream_modes.remove("messages")
+ updated_stream_modes.append("messages-tuple")
+
+ # if requested "messages-tuple",
+ # map to "messages" in requested_stream_modes
+ if "messages-tuple" in requested_stream_modes:
+ requested_stream_modes.remove("messages-tuple")
+ requested_stream_modes.append("messages")
+
+ # add 'updates' mode if not present
+ if "updates" not in updated_stream_modes:
+ updated_stream_modes.append("updates")
+
+ # remove 'events', as it's not supported in Pregel
+ if "events" in updated_stream_modes:
+ updated_stream_modes.remove("events")
+ return (updated_stream_modes, requested_stream_modes, req_single, stream)
+
+ def stream(
+ self,
+ input: dict[str, Any] | Any,
+ config: RunnableConfig | None = None,
+ *,
+ stream_mode: StreamMode | list[StreamMode] | None = None,
+ interrupt_before: All | Sequence[str] | None = None,
+ interrupt_after: All | Sequence[str] | None = None,
+ subgraphs: bool = False,
+ headers: dict[str, str] | None = None,
+ params: QueryParamTypes | None = None,
+ **kwargs: Any,
+ ) -> Iterator[dict[str, Any] | Any]:
+ """Create a run and stream the results.
+
+ This method calls `POST /threads/{thread_id}/runs/stream` if a `thread_id`
+ is speciffed in the `configurable` field of the config or
+ `POST /runs/stream` otherwise.
+
+ Args:
+ input: Input to the graph.
+ config: A `RunnableConfig` for graph invocation.
+ stream_mode: Stream mode(s) to use.
+ interrupt_before: Interrupt the graph before these nodes.
+ interrupt_after: Interrupt the graph after these nodes.
+ subgraphs: Stream from subgraphs.
+ headers: Additional headers to pass to the request.
+ **kwargs: Additional params to pass to client.runs.stream.
+
+ Yields:
+ The output of the graph.
+ """
+ sync_client = self._validate_sync_client()
+ merged_config = merge_configs(self.config, config)
+ sanitized_config = self._sanitize_config(merged_config)
+ stream_modes, requested, req_single, stream = self._get_stream_modes(
+ stream_mode, config
+ )
+ if isinstance(input, Command):
+ command: CommandSDK | None = cast(CommandSDK, asdict(input))
+ input = None
+ else:
+ command = None
+ thread_id = sanitized_config.get("configurable", {}).pop("thread_id", None)
+
+ for chunk in sync_client.runs.stream(
+ thread_id=thread_id,
+ assistant_id=self.assistant_id,
+ input=input,
+ command=command,
+ config=sanitized_config,
+ stream_mode=stream_modes,
+ interrupt_before=interrupt_before,
+ interrupt_after=interrupt_after,
+ stream_subgraphs=subgraphs or stream is not None,
+ if_not_exists="create",
+ headers=(
+ _merge_tracing_headers(headers) if self.distributed_tracing else headers
+ ),
+ params=params,
+ **kwargs,
+ ):
+ # split mode and ns
+ if NS_SEP in chunk.event:
+ mode, ns_ = chunk.event.split(NS_SEP, 1)
+ ns = tuple(ns_.split(NS_SEP))
+ else:
+ mode, ns = chunk.event, ()
+ # raise ParentCommand exception for command events
+ if mode == "command" and chunk.data.get("graph") == Command.PARENT:
+ raise ParentCommand(Command(**chunk.data))
+ # prepend caller ns (as it is not passed to remote graph)
+ if caller_ns := (config or {}).get(CONF, {}).get(CONFIG_KEY_CHECKPOINT_NS):
+ caller_ns = tuple(caller_ns.split(NS_SEP))
+ ns = caller_ns + ns
+ # stream to parent stream
+ if stream is not None and mode in stream.modes:
+ stream((ns, mode, chunk.data))
+ # raise interrupt or errors
+ if chunk.event.startswith("updates"):
+ if isinstance(chunk.data, dict) and INTERRUPT in chunk.data:
+ if caller_ns:
+ raise GraphInterrupt(
+ [Interrupt(**i) for i in chunk.data[INTERRUPT]]
+ )
+ elif chunk.event.startswith("error"):
+ raise RemoteException(chunk.data)
+ # filter for what was actually requested
+ if mode not in requested:
+ continue
+
+ if chunk.event.startswith("messages"):
+ chunk = chunk._replace(data=tuple(chunk.data)) # type: ignore
+
+ # emit chunk
+ if subgraphs:
+ if NS_SEP in chunk.event:
+ mode, ns_ = chunk.event.split(NS_SEP, 1)
+ ns = tuple(ns_.split(NS_SEP))
+ else:
+ mode, ns = chunk.event, ()
+ if req_single:
+ yield ns, chunk.data
+ else:
+ yield ns, mode, chunk.data
+ elif req_single:
+ yield chunk.data
+ else:
+ yield chunk
+
+ async def astream(
+ self,
+ input: dict[str, Any] | Any,
+ config: RunnableConfig | None = None,
+ *,
+ stream_mode: StreamMode | list[StreamMode] | None = None,
+ interrupt_before: All | Sequence[str] | None = None,
+ interrupt_after: All | Sequence[str] | None = None,
+ subgraphs: bool = False,
+ headers: dict[str, str] | None = None,
+ params: QueryParamTypes | None = None,
+ **kwargs: Any,
+ ) -> AsyncIterator[dict[str, Any] | Any]:
+ """Create a run and stream the results.
+
+ This method calls `POST /threads/{thread_id}/runs/stream` if a `thread_id`
+ is speciffed in the `configurable` field of the config or
+ `POST /runs/stream` otherwise.
+
+ Args:
+ input: Input to the graph.
+ config: A `RunnableConfig` for graph invocation.
+ stream_mode: Stream mode(s) to use.
+ interrupt_before: Interrupt the graph before these nodes.
+ interrupt_after: Interrupt the graph after these nodes.
+ subgraphs: Stream from subgraphs.
+ headers: Additional headers to pass to the request.
+ **kwargs: Additional params to pass to client.runs.stream.
+
+ Yields:
+ The output of the graph.
+ """
+ client = self._validate_client()
+ merged_config = merge_configs(self.config, config)
+ sanitized_config = self._sanitize_config(merged_config)
+ stream_modes, requested, req_single, stream = self._get_stream_modes(
+ stream_mode, config
+ )
+ if isinstance(input, Command):
+ command: CommandSDK | None = cast(CommandSDK, asdict(input))
+ input = None
+ else:
+ command = None
+ thread_id = sanitized_config.get("configurable", {}).pop("thread_id", None)
+
+ async for chunk in client.runs.stream(
+ thread_id=thread_id,
+ assistant_id=self.assistant_id,
+ input=input,
+ command=command,
+ config=sanitized_config,
+ stream_mode=stream_modes,
+ interrupt_before=interrupt_before,
+ interrupt_after=interrupt_after,
+ stream_subgraphs=subgraphs or stream is not None,
+ if_not_exists="create",
+ headers=(
+ _merge_tracing_headers(headers) if self.distributed_tracing else headers
+ ),
+ params=params,
+ **kwargs,
+ ):
+ # split mode and ns
+ if NS_SEP in chunk.event:
+ mode, ns_ = chunk.event.split(NS_SEP, 1)
+ ns = tuple(ns_.split(NS_SEP))
+ else:
+ mode, ns = chunk.event, ()
+ # raise ParentCommand exception for command events
+ if mode == "command" and chunk.data.get("graph") == Command.PARENT:
+ raise ParentCommand(Command(**chunk.data))
+ # prepend caller ns (as it is not passed to remote graph)
+ if caller_ns := (config or {}).get(CONF, {}).get(CONFIG_KEY_CHECKPOINT_NS):
+ caller_ns = tuple(caller_ns.split(NS_SEP))
+ ns = caller_ns + ns
+ # stream to parent stream
+ if stream is not None and mode in stream.modes:
+ stream((ns, mode, chunk.data))
+ # raise interrupt or errors
+ if chunk.event.startswith("updates"):
+ if isinstance(chunk.data, dict) and INTERRUPT in chunk.data:
+ if caller_ns:
+ raise GraphInterrupt(
+ [Interrupt(**i) for i in chunk.data[INTERRUPT]]
+ )
+ elif chunk.event.startswith("error"):
+ raise RemoteException(chunk.data)
+ # filter for what was actually requested
+ if mode not in requested:
+ continue
+
+ if chunk.event.startswith("messages"):
+ chunk = chunk._replace(data=tuple(chunk.data)) # type: ignore
+
+ # emit chunk
+ if subgraphs:
+ if NS_SEP in chunk.event:
+ mode, ns_ = chunk.event.split(NS_SEP, 1)
+ ns = tuple(ns_.split(NS_SEP))
+ else:
+ mode, ns = chunk.event, ()
+ if req_single:
+ yield ns, chunk.data
+ else:
+ yield ns, mode, chunk.data
+ elif req_single:
+ yield chunk.data
+ else:
+ yield chunk
+
+ async def astream_events(
+ self,
+ input: Any,
+ config: RunnableConfig | None = None,
+ *,
+ version: Literal["v1", "v2"],
+ include_names: Sequence[All] | None = None,
+ include_types: Sequence[All] | None = None,
+ include_tags: Sequence[All] | None = None,
+ exclude_names: Sequence[All] | None = None,
+ exclude_types: Sequence[All] | None = None,
+ exclude_tags: Sequence[All] | None = None,
+ **kwargs: Any,
+ ) -> AsyncIterator[dict[str, Any]]:
+ raise NotImplementedError
+
+ def invoke(
+ self,
+ input: dict[str, Any] | Any,
+ config: RunnableConfig | None = None,
+ *,
+ interrupt_before: All | Sequence[str] | None = None,
+ interrupt_after: All | Sequence[str] | None = None,
+ headers: dict[str, str] | None = None,
+ params: QueryParamTypes | None = None,
+ **kwargs: Any,
+ ) -> dict[str, Any] | Any:
+ """Create a run, wait until it finishes and return the final state.
+
+ 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.
+ headers: Additional headers to pass to the request.
+ **kwargs: Additional params to pass to RemoteGraph.stream.
+
+ Returns:
+ The output of the graph.
+ """
+ for chunk in self.stream(
+ input,
+ config=config,
+ interrupt_before=interrupt_before,
+ interrupt_after=interrupt_after,
+ headers=headers,
+ stream_mode="values",
+ params=params,
+ **kwargs,
+ ):
+ pass
+ try:
+ return chunk
+ except UnboundLocalError:
+ logger.warning("No events received from remote graph")
+ return None
+
+ async def ainvoke(
+ self,
+ input: dict[str, Any] | Any,
+ config: RunnableConfig | None = None,
+ *,
+ interrupt_before: All | Sequence[str] | None = None,
+ interrupt_after: All | Sequence[str] | None = None,
+ headers: dict[str, str] | None = None,
+ params: QueryParamTypes | None = None,
+ **kwargs: Any,
+ ) -> dict[str, Any] | Any:
+ """Create a run, wait until it finishes and return the final state.
+
+ 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.
+ headers: Additional headers to pass to the request.
+ **kwargs: Additional params to pass to RemoteGraph.astream.
+
+ Returns:
+ The output of the graph.
+ """
+ async for chunk in self.astream(
+ input,
+ config=config,
+ interrupt_before=interrupt_before,
+ interrupt_after=interrupt_after,
+ headers=headers,
+ stream_mode="values",
+ params=params,
+ **kwargs,
+ ):
+ pass
+ try:
+ return chunk
+ except UnboundLocalError:
+ logger.warning("No events received from remote graph")
+ return None
+
+
+def _merge_tracing_headers(headers: dict[str, str] | None) -> dict[str, str] | None:
+ if rt := ls.get_current_run_tree():
+ tracing_headers = rt.to_headers()
+ if headers:
+ if "baggage" in headers:
+ tracing_headers["baggage"] = (
+ f"{headers['baggage']},{tracing_headers['baggage']}"
+ )
+ headers.update(tracing_headers)
+ else:
+ headers = tracing_headers
+ return headers
diff --git a/langgraph/source/libs/langgraph/langgraph/pregel/types.py b/langgraph/source/libs/langgraph/langgraph/pregel/types.py
new file mode 100644
index 0000000000000000000000000000000000000000..39a36df68787da3cf33bbfd8a9ca8ab8b14874a2
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/pregel/types.py
@@ -0,0 +1,38 @@
+"""Re-export types moved to langgraph.types"""
+
+from langgraph.types import (
+ All,
+ CachePolicy,
+ PregelExecutableTask,
+ PregelTask,
+ RetryPolicy,
+ StateSnapshot,
+ StateUpdate,
+ StreamMode,
+ StreamWriter,
+ default_retry_on,
+)
+
+__all__ = [
+ "All",
+ "StateUpdate",
+ "CachePolicy",
+ "PregelExecutableTask",
+ "PregelTask",
+ "RetryPolicy",
+ "StateSnapshot",
+ "StreamMode",
+ "StreamWriter",
+ "default_retry_on",
+]
+
+from warnings import warn
+
+from langgraph.warnings import LangGraphDeprecatedSinceV10
+
+warn(
+ "Importing from langgraph.pregel.types is deprecated. "
+ "Please use 'from langgraph.types import ...' instead.",
+ LangGraphDeprecatedSinceV10,
+ stacklevel=2,
+)
diff --git a/langgraph/source/libs/langgraph/langgraph/py.typed b/langgraph/source/libs/langgraph/langgraph/py.typed
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/langgraph/langgraph/runtime.py b/langgraph/source/libs/langgraph/langgraph/runtime.py
new file mode 100644
index 0000000000000000000000000000000000000000..872b9987c25fbea492893d0452ee25ed69e3c3da
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/runtime.py
@@ -0,0 +1,161 @@
+from __future__ import annotations
+
+from dataclasses import dataclass, field, replace
+from typing import Any, Generic, cast
+
+from langgraph.store.base import BaseStore
+from typing_extensions import TypedDict, Unpack
+
+from langgraph._internal._constants import CONF, CONFIG_KEY_RUNTIME
+from langgraph.config import get_config
+from langgraph.types import _DC_KWARGS, StreamWriter
+from langgraph.typing import ContextT
+
+__all__ = ("Runtime", "get_runtime")
+
+
+def _no_op_stream_writer(_: Any) -> None: ...
+
+
+class _RuntimeOverrides(TypedDict, Generic[ContextT], total=False):
+ context: ContextT
+ store: BaseStore | None
+ stream_writer: StreamWriter
+ previous: Any
+
+
+@dataclass(**_DC_KWARGS)
+class Runtime(Generic[ContextT]):
+ """Convenience class that bundles run-scoped context and other runtime utilities.
+
+ This class is injected into graph nodes and middleware. It provides access to
+ `context`, `store`, `stream_writer`, and `previous`.
+
+ !!! note "Accessing `config`"
+
+ `Runtime` does not include `config`. To access `RunnableConfig`, you can inject
+ it directly by adding a `config: RunnableConfig` parameter to your node function
+ (recommended), or use `get_config()` from `langgraph.config`.
+
+ !!! note
+ `ToolRuntime` (from `langgraph.prebuilt`) is a subclass that provides similar
+ functionality but is designed specifically for tools. It shares `context`, `store`,
+ and `stream_writer` with `Runtime`, and adds tool-specific attributes like `config`,
+ `state`, and `tool_call_id`.
+
+ !!! version-added "Added in version v0.6.0"
+
+ Example:
+
+ ```python
+ from typing import TypedDict
+ from langgraph.graph import StateGraph
+ from dataclasses import dataclass
+ from langgraph.runtime import Runtime
+ from langgraph.store.memory import InMemoryStore
+
+
+ @dataclass
+ class Context: # (1)!
+ user_id: str
+
+
+ class State(TypedDict, total=False):
+ response: str
+
+
+ store = InMemoryStore() # (2)!
+ store.put(("users",), "user_123", {"name": "Alice"})
+
+
+ def personalized_greeting(state: State, runtime: Runtime[Context]) -> State:
+ '''Generate personalized greeting using runtime context and store.'''
+ user_id = runtime.context.user_id # (3)!
+ name = "unknown_user"
+ if runtime.store:
+ if memory := runtime.store.get(("users",), user_id):
+ name = memory.value["name"]
+
+ response = f"Hello {name}! Nice to see you again."
+ return {"response": response}
+
+
+ graph = (
+ StateGraph(state_schema=State, context_schema=Context)
+ .add_node("personalized_greeting", personalized_greeting)
+ .set_entry_point("personalized_greeting")
+ .set_finish_point("personalized_greeting")
+ .compile(store=store)
+ )
+
+ result = graph.invoke({}, context=Context(user_id="user_123"))
+ print(result)
+ # > {'response': 'Hello Alice! Nice to see you again.'}
+ ```
+
+ 1. Define a schema for the runtime context.
+ 2. Create a store to persist memories and other information.
+ 3. Use the runtime context to access the `user_id`.
+ """
+
+ context: ContextT = field(default=None) # type: ignore[assignment]
+ """Static context for the graph run, like `user_id`, `db_conn`, etc.
+
+ Can also be thought of as 'run dependencies'."""
+
+ store: BaseStore | None = field(default=None)
+ """Store for the graph run, enabling persistence and memory."""
+
+ stream_writer: StreamWriter = field(default=_no_op_stream_writer)
+ """Function that writes to the custom stream."""
+
+ previous: Any = field(default=None)
+ """The previous return value for the given thread.
+
+ Only available with the functional API when a checkpointer is provided.
+ """
+
+ def merge(self, other: Runtime[ContextT]) -> Runtime[ContextT]:
+ """Merge two runtimes together.
+
+ If a value is not provided in the other runtime, the value from the current runtime is used.
+ """
+ return Runtime(
+ context=other.context or self.context,
+ store=other.store or self.store,
+ stream_writer=other.stream_writer
+ if other.stream_writer is not _no_op_stream_writer
+ else self.stream_writer,
+ previous=self.previous if other.previous is None else other.previous,
+ )
+
+ def override(
+ self, **overrides: Unpack[_RuntimeOverrides[ContextT]]
+ ) -> Runtime[ContextT]:
+ """Replace the runtime with a new runtime with the given overrides."""
+ return replace(self, **overrides)
+
+
+DEFAULT_RUNTIME = Runtime(
+ context=None,
+ store=None,
+ stream_writer=_no_op_stream_writer,
+ previous=None,
+)
+
+
+def get_runtime(context_schema: type[ContextT] | None = None) -> Runtime[ContextT]:
+ """Get the runtime for the current graph run.
+
+ Args:
+ context_schema: Optional schema used for type hinting the return type of the runtime.
+
+ Returns:
+ The runtime for the current graph run.
+ """
+
+ # TODO: in an ideal world, we would have a context manager for
+ # the runtime that's independent of the config. this will follow
+ # from the removal of the configurable packing
+ runtime = cast(Runtime[ContextT], get_config()[CONF].get(CONFIG_KEY_RUNTIME))
+ return runtime
diff --git a/langgraph/source/libs/langgraph/langgraph/types.py b/langgraph/source/libs/langgraph/langgraph/types.py
new file mode 100644
index 0000000000000000000000000000000000000000..3f95deb97d6f70306e5e02171eff31bb6a2b62bb
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/types.py
@@ -0,0 +1,587 @@
+from __future__ import annotations
+
+import sys
+from collections import deque
+from collections.abc import Callable, Hashable, Sequence
+from dataclasses import asdict, dataclass
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ ClassVar,
+ Generic,
+ Literal,
+ NamedTuple,
+ TypeVar,
+ final,
+)
+from warnings import warn
+
+from langchain_core.runnables import Runnable, RunnableConfig
+from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointMetadata
+from typing_extensions import Unpack, deprecated
+from xxhash import xxh3_128_hexdigest
+
+from langgraph._internal._cache import default_cache_key
+from langgraph._internal._fields import get_cached_annotated_keys, get_update_as_tuples
+from langgraph._internal._retry import default_retry_on
+from langgraph._internal._typing import MISSING, DeprecatedKwargs
+from langgraph.warnings import LangGraphDeprecatedSinceV10
+
+if TYPE_CHECKING:
+ from langgraph.pregel.protocol import PregelProtocol
+
+
+try:
+ from langchain_core.messages.tool import ToolOutputMixin
+except ImportError:
+
+ class ToolOutputMixin: # type: ignore[no-redef]
+ pass
+
+
+__all__ = (
+ "All",
+ "Checkpointer",
+ "StreamMode",
+ "StreamWriter",
+ "RetryPolicy",
+ "CachePolicy",
+ "Interrupt",
+ "StateUpdate",
+ "PregelTask",
+ "PregelExecutableTask",
+ "StateSnapshot",
+ "Send",
+ "Command",
+ "Durability",
+ "interrupt",
+ "Overwrite",
+ "ensure_valid_checkpointer",
+)
+
+Durability = Literal["sync", "async", "exit"]
+"""Durability mode for the graph execution.
+
+- `'sync'`: Changes are persisted synchronously before the next step starts.
+- `'async'`: Changes are persisted asynchronously while the next step executes.
+- `'exit'`: Changes are persisted only when the graph exits.
+"""
+
+All = Literal["*"]
+"""Special value to indicate that graph should interrupt on all nodes."""
+
+Checkpointer = None | bool | BaseCheckpointSaver
+"""Type of the checkpointer to use for a subgraph.
+
+- `True` enables persistent checkpointing for this subgraph.
+- `False` disables checkpointing, even if the parent graph has a checkpointer.
+- `None` inherits checkpointer from the parent graph.
+"""
+
+
+def ensure_valid_checkpointer(checkpointer: Checkpointer) -> Checkpointer:
+ if checkpointer not in (None, True, False) and not isinstance(
+ checkpointer, BaseCheckpointSaver
+ ):
+ raise TypeError(
+ "Invalid checkpointer provided. Expected an instance of "
+ "`BaseCheckpointSaver`, `True`, `False`, or `None`. "
+ f"Received {type(checkpointer).__name__!s}. "
+ "Pass a proper saver (e.g., InMemorySaver, AsyncPostgresSaver)."
+ )
+ return checkpointer
+
+
+StreamMode = Literal[
+ "values", "updates", "checkpoints", "tasks", "debug", "messages", "custom"
+]
+"""How the stream method should emit outputs.
+
+- `"values"`: Emit all values in the state after each step, including interrupts.
+ When used with functional API, values are emitted once at the end of the workflow.
+- `"updates"`: Emit only the node or task names and updates returned by the nodes or tasks after each step.
+ If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are emitted separately.
+- `"custom"`: Emit custom data using from inside nodes or tasks using `StreamWriter`.
+- `"messages"`: Emit LLM messages token-by-token together with metadata for any LLM invocations inside nodes or tasks.
+- `"checkpoints"`: Emit an event when a checkpoint is created, in the same format as returned by `get_state()`.
+- `"tasks"`: Emit events when tasks start and finish, including their results and errors.
+- `"debug"`: Emit `"checkpoints"` and `"tasks"` events for debugging purposes.
+"""
+
+StreamWriter = Callable[[Any], None]
+"""`Callable` that accepts a single argument and writes it to the output stream.
+Always injected into nodes if requested as a keyword argument, but it's a no-op
+when not using `stream_mode="custom"`."""
+
+_DC_KWARGS = {"kw_only": True, "slots": True, "frozen": True}
+
+
+class RetryPolicy(NamedTuple):
+ """Configuration for retrying nodes.
+
+ !!! version-added "Added in version 0.2.24"
+ """
+
+ initial_interval: float = 0.5
+ """Amount of time that must elapse before the first retry occurs. In seconds."""
+ backoff_factor: float = 2.0
+ """Multiplier by which the interval increases after each retry."""
+ max_interval: float = 128.0
+ """Maximum amount of time that may elapse between retries. In seconds."""
+ max_attempts: int = 3
+ """Maximum number of attempts to make before giving up, including the first."""
+ jitter: bool = True
+ """Whether to add random jitter to the interval between retries."""
+ retry_on: (
+ type[Exception] | Sequence[type[Exception]] | Callable[[Exception], bool]
+ ) = default_retry_on
+ """List of exception classes that should trigger a retry, or a callable that returns `True` for exceptions that should trigger a retry."""
+
+
+KeyFuncT = TypeVar("KeyFuncT", bound=Callable[..., str | bytes])
+
+
+@dataclass(**_DC_KWARGS)
+class CachePolicy(Generic[KeyFuncT]):
+ """Configuration for caching nodes."""
+
+ key_func: KeyFuncT = default_cache_key # type: ignore[assignment]
+ """Function to generate a cache key from the node's input.
+ Defaults to hashing the input with pickle."""
+
+ ttl: int | None = None
+ """Time to live for the cache entry in seconds. If `None`, the entry never expires."""
+
+
+_DEFAULT_INTERRUPT_ID = "placeholder-id"
+
+
+@final
+@dataclass(init=False, slots=True)
+class Interrupt:
+ """Information about an interrupt that occurred in a node.
+
+ !!! version-added "Added in version 0.2.24"
+
+ !!! version-changed "Changed in version v0.4.0"
+ * `interrupt_id` was introduced as a property
+
+ !!! version-changed "Changed in version v0.6.0"
+
+ The following attributes have been removed:
+
+ * `ns`
+ * `when`
+ * `resumable`
+ * `interrupt_id`, deprecated in favor of `id`
+ """
+
+ value: Any
+ """The value associated with the interrupt."""
+
+ id: str
+ """The ID of the interrupt. Can be used to resume the interrupt directly."""
+
+ def __init__(
+ self,
+ value: Any,
+ id: str = _DEFAULT_INTERRUPT_ID,
+ **deprecated_kwargs: Unpack[DeprecatedKwargs],
+ ) -> None:
+ self.value = value
+
+ if (
+ (ns := deprecated_kwargs.get("ns", MISSING)) is not MISSING
+ and (id == _DEFAULT_INTERRUPT_ID)
+ and (isinstance(ns, Sequence))
+ ):
+ self.id = xxh3_128_hexdigest("|".join(ns).encode())
+ else:
+ self.id = id
+
+ @classmethod
+ def from_ns(cls, value: Any, ns: str) -> Interrupt:
+ return cls(value=value, id=xxh3_128_hexdigest(ns.encode()))
+
+ @property
+ @deprecated("`interrupt_id` is deprecated. Use `id` instead.", category=None)
+ def interrupt_id(self) -> str:
+ warn(
+ "`interrupt_id` is deprecated. Use `id` instead.",
+ LangGraphDeprecatedSinceV10,
+ stacklevel=2,
+ )
+ return self.id
+
+
+class StateUpdate(NamedTuple):
+ values: dict[str, Any] | None
+ as_node: str | None = None
+ task_id: str | None = None
+
+
+class PregelTask(NamedTuple):
+ """A Pregel task."""
+
+ id: str
+ name: str
+ path: tuple[str | int | tuple, ...]
+ error: Exception | None = None
+ interrupts: tuple[Interrupt, ...] = ()
+ state: None | RunnableConfig | StateSnapshot = None
+ result: Any | None = None
+
+
+if sys.version_info > (3, 11):
+ _T_DC_KWARGS = {"weakref_slot": True, "slots": True, "frozen": True}
+else:
+ _T_DC_KWARGS = {"frozen": True}
+
+
+class CacheKey(NamedTuple):
+ """Cache key for a task."""
+
+ ns: tuple[str, ...]
+ """Namespace for the cache entry."""
+ key: str
+ """Key for the cache entry."""
+ ttl: int | None
+ """Time to live for the cache entry in seconds."""
+
+
+@dataclass(**_T_DC_KWARGS)
+class PregelExecutableTask:
+ name: str
+ input: Any
+ proc: Runnable
+ writes: deque[tuple[str, Any]]
+ config: RunnableConfig
+ triggers: Sequence[str]
+ retry_policy: Sequence[RetryPolicy]
+ cache_key: CacheKey | None
+ id: str
+ path: tuple[str | int | tuple, ...]
+ writers: Sequence[Runnable] = ()
+ subgraphs: Sequence[PregelProtocol] = ()
+
+
+class StateSnapshot(NamedTuple):
+ """Snapshot of the state of the graph at the beginning of a step."""
+
+ values: dict[str, Any] | Any
+ """Current values of channels."""
+ next: tuple[str, ...]
+ """The name of the node to execute in each task for this step."""
+ config: RunnableConfig
+ """Config used to fetch this snapshot."""
+ metadata: CheckpointMetadata | None
+ """Metadata associated with this snapshot."""
+ created_at: str | None
+ """Timestamp of snapshot creation."""
+ parent_config: RunnableConfig | None
+ """Config used to fetch the parent snapshot, if any."""
+ tasks: tuple[PregelTask, ...]
+ """Tasks to execute in this step. If already attempted, may contain an error."""
+ interrupts: tuple[Interrupt, ...]
+ """Interrupts that occurred in this step that are pending resolution."""
+
+
+class Send:
+ """A message or packet to send to a specific node in the graph.
+
+ The `Send` class is used within a `StateGraph`'s conditional edges to
+ dynamically invoke a node with a custom state at the next step.
+
+ Importantly, the sent state can differ from the core graph's state,
+ allowing for flexible and dynamic workflow management.
+
+ One such example is a "map-reduce" workflow where your graph invokes
+ the same node multiple times in parallel with different states,
+ before aggregating the results back into the main graph's state.
+
+ Attributes:
+ node (str): The name of the target node to send the message to.
+ arg (Any): The state or message to send to the target node.
+
+ !!! example
+
+ ```python
+ from typing import Annotated
+ from langgraph.types import Send
+ from langgraph.graph import END, START
+ from langgraph.graph import StateGraph
+ import operator
+
+ class OverallState(TypedDict):
+ subjects: list[str]
+ jokes: Annotated[list[str], operator.add]
+
+ def continue_to_jokes(state: OverallState):
+ return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]
+
+ builder = StateGraph(OverallState)
+ builder.add_node("generate_joke", lambda state: {"jokes": [f"Joke about {state['subject']}"]})
+ builder.add_conditional_edges(START, continue_to_jokes)
+ builder.add_edge("generate_joke", END)
+ graph = builder.compile()
+
+ # Invoking with two subjects results in a generated joke for each
+ graph.invoke({"subjects": ["cats", "dogs"]})
+ # {'subjects': ['cats', 'dogs'], 'jokes': ['Joke about cats', 'Joke about dogs']}
+ ```
+ """
+
+ __slots__ = ("node", "arg")
+
+ node: str
+ arg: Any
+
+ def __init__(self, /, node: str, arg: Any) -> None:
+ """
+ Initialize a new instance of the `Send` class.
+
+ Args:
+ node: The name of the target node to send the message to.
+ arg: The state or message to send to the target node.
+ """
+ self.node = node
+ self.arg = arg
+
+ def __hash__(self) -> int:
+ return hash((self.node, self.arg))
+
+ def __repr__(self) -> str:
+ return f"Send(node={self.node!r}, arg={self.arg!r})"
+
+ def __eq__(self, value: object) -> bool:
+ return (
+ isinstance(value, Send)
+ and self.node == value.node
+ and self.arg == value.arg
+ )
+
+
+N = TypeVar("N", bound=Hashable)
+
+
+@dataclass(**_DC_KWARGS)
+class Command(Generic[N], ToolOutputMixin):
+ """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
+ - `Command.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()`][langgraph.types.interrupt].
+ Can be one of the following:
+
+ - Mapping of interrupt ids to resume values
+ - A single value with which to resume the next 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
+ """
+
+ graph: str | None = None
+ update: Any | None = None
+ resume: dict[str, Any] | Any | None = None
+ goto: Send | Sequence[Send | N] | N = ()
+
+ def __repr__(self) -> str:
+ # get all non-None values
+ contents = ", ".join(
+ f"{key}={value!r}" for key, value in asdict(self).items() if value
+ )
+ return f"Command({contents})"
+
+ def _update_as_tuples(self) -> Sequence[tuple[str, Any]]:
+ if isinstance(self.update, dict):
+ return list(self.update.items())
+ elif isinstance(self.update, (list, tuple)) and all(
+ isinstance(t, tuple) and len(t) == 2 and isinstance(t[0], str)
+ for t in self.update
+ ):
+ return self.update
+ elif keys := get_cached_annotated_keys(type(self.update)):
+ return get_update_as_tuples(self.update, keys)
+ elif self.update is not None:
+ return [("__root__", self.update)]
+ else:
+ return []
+
+ PARENT: ClassVar[Literal["__parent__"]] = "__parent__"
+
+
+def interrupt(value: Any) -> Any:
+ """Interrupt the graph with a resumable exception from within a node.
+
+ The `interrupt` function enables human-in-the-loop workflows by pausing graph
+ execution and surfacing a value to the client. This value can communicate context
+ or request input required to resume execution.
+
+ In a given node, the first invocation of this function raises a `GraphInterrupt`
+ exception, halting execution. The provided `value` is included with the exception
+ and sent to the client executing the graph.
+
+ A client resuming the graph must use the [`Command`][langgraph.types.Command]
+ primitive to specify a value for the interrupt and continue execution.
+ The graph resumes from the start of the node, **re-executing** all logic.
+
+ If a node contains multiple `interrupt` calls, LangGraph matches resume values
+ to interrupts based on their order in the node. This list of resume values
+ is scoped to the specific task executing the node and is not shared across tasks.
+
+ To use an `interrupt`, you must enable a checkpointer, as the feature relies
+ on persisting the graph state.
+
+ !!! example
+
+ ```python
+ import uuid
+ from typing import Optional
+ from typing_extensions import TypedDict
+
+ from langgraph.checkpoint.memory import InMemorySaver
+ from langgraph.constants import START
+ from langgraph.graph import StateGraph
+ from langgraph.types import interrupt, Command
+
+
+ class State(TypedDict):
+ \"\"\"The graph state.\"\"\"
+
+ foo: str
+ human_value: Optional[str]
+ \"\"\"Human value will be updated using an interrupt.\"\"\"
+
+
+ def node(state: State):
+ answer = interrupt(
+ # This value will be sent to the client
+ # as part of the interrupt information.
+ \"what is your age?\"
+ )
+ print(f\"> Received an input from the interrupt: {answer}\")
+ return {\"human_value\": answer}
+
+
+ builder = StateGraph(State)
+ builder.add_node(\"node\", node)
+ builder.add_edge(START, \"node\")
+
+ # A checkpointer must be enabled for interrupts to work!
+ checkpointer = InMemorySaver()
+ graph = builder.compile(checkpointer=checkpointer)
+
+ config = {
+ \"configurable\": {
+ \"thread_id\": uuid.uuid4(),
+ }
+ }
+
+ for chunk in graph.stream({\"foo\": \"abc\"}, config):
+ print(chunk)
+
+ # > {'__interrupt__': (Interrupt(value='what is your age?', id='45fda8478b2ef754419799e10992af06'),)}
+
+ command = Command(resume=\"some input from a human!!!\")
+
+ for chunk in graph.stream(Command(resume=\"some input from a human!!!\"), config):
+ print(chunk)
+
+ # > Received an input from the interrupt: some input from a human!!!
+ # > {'node': {'human_value': 'some input from a human!!!'}}
+ ```
+
+ Args:
+ value: The value to surface to the client when the graph is interrupted.
+
+ Returns:
+ Any: On subsequent invocations within the same node (same task to be precise), returns the value provided during the first invocation
+
+ Raises:
+ GraphInterrupt: On the first invocation within the node, halts execution and surfaces the provided value to the client.
+ """
+ from langgraph._internal._constants import (
+ CONFIG_KEY_CHECKPOINT_NS,
+ CONFIG_KEY_SCRATCHPAD,
+ CONFIG_KEY_SEND,
+ RESUME,
+ )
+ from langgraph.config import get_config
+ from langgraph.errors import GraphInterrupt
+
+ conf = get_config()["configurable"]
+ # track interrupt index
+ scratchpad = conf[CONFIG_KEY_SCRATCHPAD]
+ idx = scratchpad.interrupt_counter()
+ # find previous resume values
+ if scratchpad.resume:
+ if idx < len(scratchpad.resume):
+ conf[CONFIG_KEY_SEND]([(RESUME, scratchpad.resume)])
+ return scratchpad.resume[idx]
+ # find current resume value
+ v = scratchpad.get_null_resume(True)
+ if v is not None:
+ assert len(scratchpad.resume) == idx, (scratchpad.resume, idx)
+ scratchpad.resume.append(v)
+ conf[CONFIG_KEY_SEND]([(RESUME, scratchpad.resume)])
+ return v
+ # no resume value found
+ raise GraphInterrupt(
+ (
+ Interrupt.from_ns(
+ value=value,
+ ns=conf[CONFIG_KEY_CHECKPOINT_NS],
+ ),
+ )
+ )
+
+
+@dataclass(slots=True)
+class Overwrite:
+ """Bypass a reducer and write the wrapped value directly to a `BinaryOperatorAggregate` channel.
+
+ Receiving multiple `Overwrite` values for the same channel in a single super-step
+ will raise an `InvalidUpdateError`.
+
+ !!! example
+
+ ```python
+ from typing import Annotated
+ import operator
+ from langgraph.graph import StateGraph
+ from langgraph.types import Overwrite
+
+ class State(TypedDict):
+ messages: Annotated[list, operator.add]
+
+ def node_a(state: TypedDict):
+ # Normal update: uses the reducer (operator.add)
+ return {"messages": ["a"]}
+
+ def node_b(state: State):
+ # Overwrite: bypasses the reducer and replaces the entire value
+ return {"messages": Overwrite(value=["b"])}
+
+ builder = StateGraph(State)
+ builder.add_node("node_a", node_a)
+ builder.add_node("node_b", node_b)
+ builder.set_entry_point("node_a")
+ builder.add_edge("node_a", "node_b")
+ graph = builder.compile()
+
+ # Without Overwrite in node_b, messages would be ["START", "a", "b"]
+ # With Overwrite, messages is just ["b"]
+ result = graph.invoke({"messages": ["START"]})
+ assert result == {"messages": ["b"]}
+ ```
+ """
+
+ value: Any
+ """The value to write directly to the channel, bypassing any reducer."""
diff --git a/langgraph/source/libs/langgraph/langgraph/typing.py b/langgraph/source/libs/langgraph/langgraph/typing.py
new file mode 100644
index 0000000000000000000000000000000000000000..c1187357baab7f3e3de85ac0a7905292843c4c70
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/typing.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from typing_extensions import TypeVar
+
+from langgraph._internal._typing import StateLike
+
+__all__ = (
+ "StateT",
+ "StateT_co",
+ "StateT_contra",
+ "InputT",
+ "OutputT",
+ "ContextT",
+)
+
+StateT = TypeVar("StateT", bound=StateLike)
+"""Type variable used to represent the state in a graph."""
+
+StateT_co = TypeVar("StateT_co", bound=StateLike, covariant=True)
+
+StateT_contra = TypeVar("StateT_contra", bound=StateLike, contravariant=True)
+
+ContextT = TypeVar("ContextT", bound=StateLike | None, default=None)
+"""Type variable used to represent graph run scoped context.
+
+Defaults to `None`.
+"""
+
+ContextT_contra = TypeVar(
+ "ContextT_contra", bound=StateLike | None, contravariant=True, default=None
+)
+
+InputT = TypeVar("InputT", bound=StateLike, default=StateT)
+"""Type variable used to represent the input to a `StateGraph`.
+
+Defaults to `StateT`.
+"""
+
+OutputT = TypeVar("OutputT", bound=StateLike, default=StateT)
+"""Type variable used to represent the output of a `StateGraph`.
+
+Defaults to `StateT`.
+"""
+
+NodeInputT = TypeVar("NodeInputT", bound=StateLike)
+"""Type variable used to represent the input to a node."""
+
+NodeInputT_contra = TypeVar("NodeInputT_contra", bound=StateLike, contravariant=True)
diff --git a/langgraph/source/libs/langgraph/langgraph/utils/__init__.py b/langgraph/source/libs/langgraph/langgraph/utils/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..d0f11b2b494ae7eecd26b675a550efeb6de20cf5
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/utils/__init__.py
@@ -0,0 +1 @@
+"""Legacy utilities module, to be removed in v1."""
diff --git a/langgraph/source/libs/langgraph/langgraph/utils/config.py b/langgraph/source/libs/langgraph/langgraph/utils/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..f855d0a12dc617a0842d008a73a4b2a8356ed7c9
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/utils/config.py
@@ -0,0 +1,4 @@
+"""Backwards compat imports for config utilities, to be removed in v1."""
+
+from langgraph._internal._config import ensure_config, patch_configurable # noqa: F401
+from langgraph.config import get_config, get_store # noqa: F401
diff --git a/langgraph/source/libs/langgraph/langgraph/utils/runnable.py b/langgraph/source/libs/langgraph/langgraph/utils/runnable.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e7ccc668b8dc1cd4061e4f550438eac5cc780c8
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/utils/runnable.py
@@ -0,0 +1,3 @@
+"""Backwards compat imports for runnable utilities, to be removed in v1."""
+
+from langgraph._internal._runnable import RunnableCallable, RunnableLike # noqa: F401
diff --git a/langgraph/source/libs/langgraph/langgraph/version.py b/langgraph/source/libs/langgraph/langgraph/version.py
new file mode 100644
index 0000000000000000000000000000000000000000..a81f647c745a56fe11179db19bbbefb81d7a2e7e
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/version.py
@@ -0,0 +1,12 @@
+"""Exports package version."""
+
+from importlib import metadata
+
+__all__ = ("__version__",)
+
+try:
+ __version__ = metadata.version(__package__)
+except metadata.PackageNotFoundError:
+ # Case where package metadata is not available.
+ __version__ = ""
+del metadata # optional, avoids polluting the results of dir(__package__)
diff --git a/langgraph/source/libs/langgraph/langgraph/warnings.py b/langgraph/source/libs/langgraph/langgraph/warnings.py
new file mode 100644
index 0000000000000000000000000000000000000000..638f247e3f260474c38578605c6a863377e6271b
--- /dev/null
+++ b/langgraph/source/libs/langgraph/langgraph/warnings.py
@@ -0,0 +1,61 @@
+"""LangGraph specific warnings."""
+
+from __future__ import annotations
+
+__all__ = (
+ "LangGraphDeprecationWarning",
+ "LangGraphDeprecatedSinceV05",
+ "LangGraphDeprecatedSinceV10",
+)
+
+
+class LangGraphDeprecationWarning(DeprecationWarning):
+ """A LangGraph specific deprecation warning.
+
+ Attributes:
+ message: Description of the warning.
+ since: LangGraph version in which the deprecation was introduced.
+ expected_removal: LangGraph version in what the corresponding functionality expected to be removed.
+
+ Inspired by the Pydantic `PydanticDeprecationWarning` class, which sets a great standard
+ for deprecation warnings with clear versioning information.
+ """
+
+ message: str
+ since: tuple[int, int]
+ expected_removal: tuple[int, int]
+
+ def __init__(
+ self,
+ message: str,
+ *args: object,
+ since: tuple[int, int],
+ expected_removal: tuple[int, int] | None = None,
+ ) -> None:
+ super().__init__(message, *args)
+ self.message = message.rstrip(".")
+ self.since = since
+ self.expected_removal = (
+ expected_removal if expected_removal is not None else (since[0] + 1, 0)
+ )
+
+ def __str__(self) -> str:
+ message = (
+ f"{self.message}. Deprecated in LangGraph V{self.since[0]}.{self.since[1]}"
+ f" to be removed in V{self.expected_removal[0]}.{self.expected_removal[1]}."
+ )
+ return message
+
+
+class LangGraphDeprecatedSinceV05(LangGraphDeprecationWarning):
+ """A specific `LangGraphDeprecationWarning` subclass defining functionality deprecated since LangGraph v0.5.0"""
+
+ def __init__(self, message: str, *args: object) -> None:
+ super().__init__(message, *args, since=(0, 5), expected_removal=(2, 0))
+
+
+class LangGraphDeprecatedSinceV10(LangGraphDeprecationWarning):
+ """A specific `LangGraphDeprecationWarning` subclass defining functionality deprecated since LangGraph v1.0.0"""
+
+ def __init__(self, message: str, *args: object) -> None:
+ super().__init__(message, *args, since=(1, 0), expected_removal=(2, 0))
diff --git a/langgraph/source/libs/langgraph/pyproject.toml b/langgraph/source/libs/langgraph/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..4e9be99bb50e789e8266ebed838f3d4e887e23b9
--- /dev/null
+++ b/langgraph/source/libs/langgraph/pyproject.toml
@@ -0,0 +1,128 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "langgraph"
+version = "1.0.8"
+description = "Building stateful, multi-actor applications with LLMs"
+authors = []
+requires-python = ">=3.10"
+readme = "README.md"
+license = "MIT"
+license-files = ['LICENSE']
+classifiers = [
+ 'Development Status :: 5 - Production/Stable',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: Implementation :: CPython',
+ 'Programming Language :: Python :: Implementation :: PyPy',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3 :: Only',
+ 'Programming Language :: Python :: 3.10',
+ 'Programming Language :: Python :: 3.11',
+ 'Programming Language :: Python :: 3.12',
+ 'Programming Language :: Python :: 3.13',
+]
+dependencies = [
+ "langchain-core>=0.1",
+ "langgraph-checkpoint>=2.1.0,<5.0.0",
+ "langgraph-sdk>=0.3.0,<0.4.0",
+ "langgraph-prebuilt>=1.0.7,<1.1.0",
+ "xxhash>=3.5.0",
+ "pydantic>=2.7.4",
+]
+
+
+[project.urls]
+Homepage = "https://docs.langchain.com/oss/python/langgraph/overview"
+Documentation = "https://reference.langchain.com/python/langgraph/"
+Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/langgraph"
+Changelog = "https://github.com/langchain-ai/langgraph/releases"
+Twitter = "https://x.com/LangChain"
+Slack = "https://www.langchain.com/join-community"
+Reddit = "https://www.reddit.com/r/LangChain/"
+
+[dependency-groups]
+test = [
+ "pytest",
+ "pytest-cov",
+ "pytest-dotenv",
+ "pytest-mock",
+ "syrupy",
+ "httpx",
+ "pytest-watcher",
+ "pytest-xdist[psutil]",
+ "pytest-repeat",
+ "langchain-core>=1.0.0",
+ "langgraph-prebuilt",
+ "langgraph-checkpoint",
+ "langgraph-checkpoint-sqlite",
+ "langgraph-checkpoint-postgres",
+ "langgraph-sdk",
+ "psycopg[binary]",
+ "uvloop==0.21.0beta1",
+ "pyperf",
+ "py-spy",
+ "pycryptodome",
+ "langgraph-cli; python_version < '3.14'",
+ "langgraph-cli[inmem]; python_version < '3.14'",
+ "redis",
+]
+lint = [
+ "mypy",
+ "ruff",
+ "types-requests",
+]
+dev = [
+ {include-group = "test"},
+ {include-group = "lint"},
+ "jupyter",
+]
+
+
+[tool.uv.sources]
+langgraph-prebuilt = { path = "../prebuilt", editable = true }
+langgraph-checkpoint = { path = "../checkpoint", editable = true }
+langgraph-checkpoint-sqlite = { path = "../checkpoint-sqlite", editable = true }
+langgraph-checkpoint-postgres = { path = "../checkpoint-postgres", editable = true }
+langgraph-sdk = { path = "../sdk-py", editable = true }
+langgraph-cli = { path = "../cli", editable = true }
+
+[tool.ruff]
+lint.select = [ "E", "F", "I", "TID251", "UP" ]
+lint.ignore = [ "E501" ]
+line-length = 88
+indent-width = 4
+extend-include = ["*.ipynb"]
+target-version = "py310"
+
+[tool.ruff.lint.flake8-tidy-imports.banned-api]
+"typing.TypedDict".msg = "Use typing_extensions.TypedDict instead."
+
+[tool.mypy]
+# https://mypy.readthedocs.io/en/stable/config_file.html
+disallow_untyped_defs = "True"
+explicit_package_bases = "True"
+warn_no_return = "False"
+warn_unused_ignores = "True"
+warn_redundant_casts = "True"
+allow_redefinition = "True"
+disable_error_code = "typeddict-item, return-value, override, has-type"
+
+[tool.coverage.run]
+omit = ["tests/*"]
+
+[tool.pytest-watcher]
+now = true
+delay = 0.1
+patterns = ["*.py"]
+
+[tool.hatch.build.targets.wheel]
+packages = ["langgraph"]
+
+[tool.pytest.ini_options]
+addopts = "--full-trace --strict-markers --strict-config --durations=5 --snapshot-warn-unused"
+
+[tool.codespell]
+# Ignore words specific to the LangGraph library code
+ignore-words-list = "infor,thead,stdio,nd,jupyter,lets,lite,uis,deque,langgraph,langchain,pydantic,typing,async,await,coroutine,iterable,iterables,serializable,deserializable,checkpointer,checkpointing,stateful,statefulness,prebuilt,prebuilt,supervisor,supervisory,swarm,swarming,multiactor,multiactors,subgraph,subgraphs,workflow,workflows,streaming,streamable,streamed,streamer,streamers,streaming,streamable,streamed,streamer,streamers"
diff --git a/langgraph/source/libs/langgraph/tests/__init__.py b/langgraph/source/libs/langgraph/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/langgraph/source/libs/langgraph/tests/__snapshots__/test_large_cases.ambr b/langgraph/source/libs/langgraph/tests/__snapshots__/test_large_cases.ambr
new file mode 100644
index 0000000000000000000000000000000000000000..27e9c29048370199c57e28dc0c3e383b2671938d
--- /dev/null
+++ b/langgraph/source/libs/langgraph/tests/__snapshots__/test_large_cases.ambr
@@ -0,0 +1,311 @@
+# serializer version: 1
+# name: test_conditional_state_graph[memory]
+ '{"$defs": {"AgentAction": {"description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"additionalProperties": true, "title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "title": "Agent Outcome"}, "intermediate_steps": {"items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "title": "AgentState", "type": "object"}'
+# ---
+# name: test_conditional_state_graph[memory].1
+ '{"$defs": {"AgentAction": {"description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"additionalProperties": true, "title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "title": "Agent Outcome"}, "intermediate_steps": {"items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "title": "AgentState", "type": "object"}'
+# ---
+# name: test_conditional_state_graph[memory].2
+ '''
+ {
+ "nodes": [
+ {
+ "id": "__start__",
+ "type": "runnable",
+ "data": {
+ "id": [
+ "langgraph",
+ "_internal",
+ "_runnable",
+ "RunnableCallable"
+ ],
+ "name": "__start__"
+ }
+ },
+ {
+ "id": "agent",
+ "type": "runnable",
+ "data": {
+ "id": [
+ "langchain",
+ "schema",
+ "runnable",
+ "RunnableSequence"
+ ],
+ "name": "agent"
+ }
+ },
+ {
+ "id": "tools",
+ "type": "runnable",
+ "data": {
+ "id": [
+ "langgraph",
+ "_internal",
+ "_runnable",
+ "RunnableCallable"
+ ],
+ "name": "tools"
+ }
+ },
+ {
+ "id": "__end__"
+ }
+ ],
+ "edges": [
+ {
+ "source": "__start__",
+ "target": "agent"
+ },
+ {
+ "source": "agent",
+ "target": "__end__",
+ "data": "exit",
+ "conditional": true
+ },
+ {
+ "source": "agent",
+ "target": "tools",
+ "data": "continue",
+ "conditional": true
+ },
+ {
+ "source": "tools",
+ "target": "agent"
+ }
+ ]
+ }
+ '''
+# ---
+# name: test_conditional_state_graph[memory].3
+ '''
+ graph TD;
+ __start__ --> agent;
+ agent -. exit .-> __end__;
+ agent -. continue .-> tools;
+ tools --> agent;
+
+ '''
+# ---
+# name: test_message_graph[memory]
+ '{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAn `AIMessage` is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model and standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_calls": {"items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI (yielded when streaming).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_calls": {"items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}, "chunk_position": {"anyOf": [{"const": "last", "type": "string"}, {"type": "null"}], "default": null, "title": "Chunk Position"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\n`FunctionMessage` are an older version of the `ToolMessage` schema, and\\ndo not contain the `tool_call_id` field.\\n\\nThe `tool_call_id` field is used to associate the tool call request with the\\ntool call response. Useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "function", "default": "function", "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessage", "type": "object"}, "FunctionMessageChunk": {"additionalProperties": true, "description": "Function Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "FunctionMessageChunk", "default": "FunctionMessageChunk", "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessageChunk", "type": "object"}, "HumanMessage": {"additionalProperties": true, "description": "Message from the user.\\n\\nA `HumanMessage` is a message that is passed in from a user to the model.\\n\\nExample:\\n ```python\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(content=\\"You are a helpful assistant! Your name is Bob.\\"),\\n HumanMessage(content=\\"What is your name?\\"),\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))\\n ```", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "human", "default": "human", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "HumanMessage", "type": "object"}, "HumanMessageChunk": {"additionalProperties": true, "description": "Human Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "HumanMessageChunk", "default": "HumanMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "HumanMessageChunk", "type": "object"}, "InputTokenDetails": {"additionalProperties": true, "description": "Breakdown of input token counts.\\n\\nDoes *not* need to sum to full input token count. Does *not* need to have all keys.\\n\\nExample:\\n ```python\\n {\\n \\"audio\\": 10,\\n \\"cache_creation\\": 200,\\n \\"cache_read\\": 100,\\n }\\n ```\\n\\nMay also hold extra provider-specific keys.\\n\\n!!! version-added \\"Added in `langchain-core` 0.3.9\\"", "properties": {"audio": {"title": "Audio", "type": "integer"}, "cache_creation": {"title": "Cache Creation", "type": "integer"}, "cache_read": {"title": "Cache Read", "type": "integer"}}, "title": "InputTokenDetails", "type": "object"}, "InvalidToolCall": {"additionalProperties": true, "description": "Allowance for errors made by LLM.\\n\\nHere we add an `error` key to surface errors made during generation\\n(e.g., invalid JSON arguments.)", "properties": {"type": {"const": "invalid_tool_call", "title": "Type", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "error": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Error"}, "index": {"anyOf": [{"type": "integer"}, {"type": "string"}], "title": "Index"}, "extras": {"additionalProperties": true, "title": "Extras", "type": "object"}}, "required": ["type", "id", "name", "args", "error"], "title": "InvalidToolCall", "type": "object"}, "OutputTokenDetails": {"additionalProperties": true, "description": "Breakdown of output token counts.\\n\\nDoes *not* need to sum to full output token count. Does *not* need to have all keys.\\n\\nExample:\\n ```python\\n {\\n \\"audio\\": 10,\\n \\"reasoning\\": 200,\\n }\\n ```\\n\\nMay also hold extra provider-specific keys.\\n\\n!!! version-added \\"Added in `langchain-core` 0.3.9\\"", "properties": {"audio": {"title": "Audio", "type": "integer"}, "reasoning": {"title": "Reasoning", "type": "integer"}}, "title": "OutputTokenDetails", "type": "object"}, "SystemMessage": {"additionalProperties": true, "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n ```python\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(content=\\"You are a helpful assistant! Your name is Bob.\\"),\\n HumanMessage(content=\\"What is your name?\\"),\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))\\n ```", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "system", "default": "system", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessage", "type": "object"}, "SystemMessageChunk": {"additionalProperties": true, "description": "System Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "SystemMessageChunk", "default": "SystemMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessageChunk", "type": "object"}, "ToolCall": {"additionalProperties": true, "description": "Represents an AI\'s request to call a tool.\\n\\nExample:\\n ```python\\n {\\"name\\": \\"foo\\", \\"args\\": {\\"a\\": 1}, \\"id\\": \\"123\\"}\\n ```\\n\\n This represents a request to call the tool named `\'foo\'` with arguments\\n `{\\"a\\": 1}` and an identifier of `\'123\'`.", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"additionalProperties": true, "title": "Args", "type": "object"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "type": {"const": "tool_call", "title": "Type", "type": "string"}}, "required": ["name", "args", "id"], "title": "ToolCall", "type": "object"}, "ToolCallChunk": {"additionalProperties": true, "description": "A chunk of a tool call (yielded when streaming).\\n\\nWhen merging `ToolCallChunk`s (e.g., via `AIMessageChunk.__add__`),\\nall string attributes are concatenated. Chunks are only merged if their\\nvalues of `index` are equal and not None.\\n\\nExample:\\n```python\\nleft_chunks = [ToolCallChunk(name=\\"foo\\", args=\'{\\"a\\":\', index=0)]\\nright_chunks = [ToolCallChunk(name=None, args=\\"1}\\", index=0)]\\n\\n(\\n AIMessageChunk(content=\\"\\", tool_call_chunks=left_chunks)\\n + AIMessageChunk(content=\\"\\", tool_call_chunks=right_chunks)\\n).tool_call_chunks == [ToolCallChunk(name=\\"foo\\", args=\'{\\"a\\":1}\', index=0)]\\n```", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "index": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Index"}, "type": {"const": "tool_call_chunk", "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "index"], "title": "ToolCallChunk", "type": "object"}, "ToolMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\n`ToolMessage` objects contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\n`tool_call_id` is used to associate the tool call request with the tool call\\nresponse. Useful in situations where a chat model is able to request multiple tool\\ncalls in parallel.\\n\\nExample:\\n A `ToolMessage` representing a result of `42` from a tool call with id\\n\\n ```python\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\\"42\\", tool_call_id=\\"call_Jja7J89XsjrOLA5r!MEOW!SL\\")\\n ```\\n\\nExample:\\n A `ToolMessage` where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n ```python\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between \\"\\n \\"x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\\"call_Jja7J89XsjrOLA5r!MEOW!SL\\",\\n )\\n ```", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "tool", "default": "tool", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessage", "type": "object"}, "ToolMessageChunk": {"additionalProperties": true, "description": "Tool Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ToolMessageChunk", "default": "ToolMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessageChunk", "type": "object"}, "UsageMetadata": {"additionalProperties": true, "description": "Usage metadata for a message, such as token counts.\\n\\nThis is a standard representation of token usage that is consistent across models.\\n\\nExample:\\n ```python\\n {\\n \\"input_tokens\\": 350,\\n \\"output_tokens\\": 240,\\n \\"total_tokens\\": 590,\\n \\"input_token_details\\": {\\n \\"audio\\": 10,\\n \\"cache_creation\\": 200,\\n \\"cache_read\\": 100,\\n },\\n \\"output_token_details\\": {\\n \\"audio\\": 10,\\n \\"reasoning\\": 200,\\n },\\n }\\n ```\\n\\n!!! warning \\"Behavior changed in `langchain-core` 0.3.9\\"\\n\\n Added `input_token_details` and `output_token_details`.\\n\\n!!! note \\"LangSmith SDK\\"\\n\\n The LangSmith SDK also has a `UsageMetadata` class. While the two share fields,\\n LangSmith\'s `UsageMetadata` has additional fields to capture cost information\\n used by the LangSmith platform.", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}, "input_token_details": {"$ref": "#/$defs/InputTokenDetails"}, "output_token_details": {"$ref": "#/$defs/OutputTokenDetails"}}, "required": ["input_tokens", "output_tokens", "total_tokens"], "title": "UsageMetadata", "type": "object"}}, "default": null, "items": {"oneOf": [{"$ref": "#/$defs/AIMessage"}, {"$ref": "#/$defs/HumanMessage"}, {"$ref": "#/$defs/ChatMessage"}, {"$ref": "#/$defs/SystemMessage"}, {"$ref": "#/$defs/FunctionMessage"}, {"$ref": "#/$defs/ToolMessage"}, {"$ref": "#/$defs/AIMessageChunk"}, {"$ref": "#/$defs/HumanMessageChunk"}, {"$ref": "#/$defs/ChatMessageChunk"}, {"$ref": "#/$defs/SystemMessageChunk"}, {"$ref": "#/$defs/FunctionMessageChunk"}, {"$ref": "#/$defs/ToolMessageChunk"}]}, "title": "LangGraphInput", "type": "array"}'
+# ---
+# name: test_message_graph[memory].1
+ '{"$defs": {"AIMessage": {"additionalProperties": true, "description": "Message from an AI.\\n\\nAn `AIMessage` is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model and standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ai", "default": "ai", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_calls": {"items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}}, "required": ["content"], "title": "AIMessage", "type": "object"}, "AIMessageChunk": {"additionalProperties": true, "description": "Message chunk from an AI (yielded when streaming).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "AIMessageChunk", "default": "AIMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_calls": {"items": {"$ref": "#/$defs/ToolCall"}, "title": "Tool Calls", "type": "array"}, "invalid_tool_calls": {"items": {"$ref": "#/$defs/InvalidToolCall"}, "title": "Invalid Tool Calls", "type": "array"}, "usage_metadata": {"anyOf": [{"$ref": "#/$defs/UsageMetadata"}, {"type": "null"}], "default": null}, "tool_call_chunks": {"items": {"$ref": "#/$defs/ToolCallChunk"}, "title": "Tool Call Chunks", "type": "array"}, "chunk_position": {"anyOf": [{"const": "last", "type": "string"}, {"type": "null"}], "default": null, "title": "Chunk Position"}}, "required": ["content"], "title": "AIMessageChunk", "type": "object"}, "ChatMessage": {"additionalProperties": true, "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "chat", "default": "chat", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessage", "type": "object"}, "ChatMessageChunk": {"additionalProperties": true, "description": "Chat Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ChatMessageChunk", "default": "ChatMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"], "title": "ChatMessageChunk", "type": "object"}, "FunctionMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\n`FunctionMessage` are an older version of the `ToolMessage` schema, and\\ndo not contain the `tool_call_id` field.\\n\\nThe `tool_call_id` field is used to associate the tool call request with the\\ntool call response. Useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "function", "default": "function", "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessage", "type": "object"}, "FunctionMessageChunk": {"additionalProperties": true, "description": "Function Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "FunctionMessageChunk", "default": "FunctionMessageChunk", "title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "name"], "title": "FunctionMessageChunk", "type": "object"}, "HumanMessage": {"additionalProperties": true, "description": "Message from the user.\\n\\nA `HumanMessage` is a message that is passed in from a user to the model.\\n\\nExample:\\n ```python\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(content=\\"You are a helpful assistant! Your name is Bob.\\"),\\n HumanMessage(content=\\"What is your name?\\"),\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))\\n ```", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "human", "default": "human", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "HumanMessage", "type": "object"}, "HumanMessageChunk": {"additionalProperties": true, "description": "Human Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "HumanMessageChunk", "default": "HumanMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "HumanMessageChunk", "type": "object"}, "InputTokenDetails": {"additionalProperties": true, "description": "Breakdown of input token counts.\\n\\nDoes *not* need to sum to full input token count. Does *not* need to have all keys.\\n\\nExample:\\n ```python\\n {\\n \\"audio\\": 10,\\n \\"cache_creation\\": 200,\\n \\"cache_read\\": 100,\\n }\\n ```\\n\\nMay also hold extra provider-specific keys.\\n\\n!!! version-added \\"Added in `langchain-core` 0.3.9\\"", "properties": {"audio": {"title": "Audio", "type": "integer"}, "cache_creation": {"title": "Cache Creation", "type": "integer"}, "cache_read": {"title": "Cache Read", "type": "integer"}}, "title": "InputTokenDetails", "type": "object"}, "InvalidToolCall": {"additionalProperties": true, "description": "Allowance for errors made by LLM.\\n\\nHere we add an `error` key to surface errors made during generation\\n(e.g., invalid JSON arguments.)", "properties": {"type": {"const": "invalid_tool_call", "title": "Type", "type": "string"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "error": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Error"}, "index": {"anyOf": [{"type": "integer"}, {"type": "string"}], "title": "Index"}, "extras": {"additionalProperties": true, "title": "Extras", "type": "object"}}, "required": ["type", "id", "name", "args", "error"], "title": "InvalidToolCall", "type": "object"}, "OutputTokenDetails": {"additionalProperties": true, "description": "Breakdown of output token counts.\\n\\nDoes *not* need to sum to full output token count. Does *not* need to have all keys.\\n\\nExample:\\n ```python\\n {\\n \\"audio\\": 10,\\n \\"reasoning\\": 200,\\n }\\n ```\\n\\nMay also hold extra provider-specific keys.\\n\\n!!! version-added \\"Added in `langchain-core` 0.3.9\\"", "properties": {"audio": {"title": "Audio", "type": "integer"}, "reasoning": {"title": "Reasoning", "type": "integer"}}, "title": "OutputTokenDetails", "type": "object"}, "SystemMessage": {"additionalProperties": true, "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n ```python\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(content=\\"You are a helpful assistant! Your name is Bob.\\"),\\n HumanMessage(content=\\"What is your name?\\"),\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))\\n ```", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "system", "default": "system", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessage", "type": "object"}, "SystemMessageChunk": {"additionalProperties": true, "description": "System Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "SystemMessageChunk", "default": "SystemMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content"], "title": "SystemMessageChunk", "type": "object"}, "ToolCall": {"additionalProperties": true, "description": "Represents an AI\'s request to call a tool.\\n\\nExample:\\n ```python\\n {\\"name\\": \\"foo\\", \\"args\\": {\\"a\\": 1}, \\"id\\": \\"123\\"}\\n ```\\n\\n This represents a request to call the tool named `\'foo\'` with arguments\\n `{\\"a\\": 1}` and an identifier of `\'123\'`.", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"additionalProperties": true, "title": "Args", "type": "object"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "type": {"const": "tool_call", "title": "Type", "type": "string"}}, "required": ["name", "args", "id"], "title": "ToolCall", "type": "object"}, "ToolCallChunk": {"additionalProperties": true, "description": "A chunk of a tool call (yielded when streaming).\\n\\nWhen merging `ToolCallChunk`s (e.g., via `AIMessageChunk.__add__`),\\nall string attributes are concatenated. Chunks are only merged if their\\nvalues of `index` are equal and not None.\\n\\nExample:\\n```python\\nleft_chunks = [ToolCallChunk(name=\\"foo\\", args=\'{\\"a\\":\', index=0)]\\nright_chunks = [ToolCallChunk(name=None, args=\\"1}\\", index=0)]\\n\\n(\\n AIMessageChunk(content=\\"\\", tool_call_chunks=left_chunks)\\n + AIMessageChunk(content=\\"\\", tool_call_chunks=right_chunks)\\n).tool_call_chunks == [ToolCallChunk(name=\\"foo\\", args=\'{\\"a\\":1}\', index=0)]\\n```", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Name"}, "args": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Args"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Id"}, "index": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Index"}, "type": {"const": "tool_call_chunk", "title": "Type", "type": "string"}}, "required": ["name", "args", "id", "index"], "title": "ToolCallChunk", "type": "object"}, "ToolMessage": {"additionalProperties": true, "description": "Message for passing the result of executing a tool back to a model.\\n\\n`ToolMessage` objects contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\n`tool_call_id` is used to associate the tool call request with the tool call\\nresponse. Useful in situations where a chat model is able to request multiple tool\\ncalls in parallel.\\n\\nExample:\\n A `ToolMessage` representing a result of `42` from a tool call with id\\n\\n ```python\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\\"42\\", tool_call_id=\\"call_Jja7J89XsjrOLA5r!MEOW!SL\\")\\n ```\\n\\nExample:\\n A `ToolMessage` where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n ```python\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between \\"\\n \\"x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\\"call_Jja7J89XsjrOLA5r!MEOW!SL\\",\\n )\\n ```", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "tool", "default": "tool", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessage", "type": "object"}, "ToolMessageChunk": {"additionalProperties": true, "description": "Tool Message chunk.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"const": "ToolMessageChunk", "default": "ToolMessageChunk", "title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"default": null, "title": "Artifact"}, "status": {"default": "success", "enum": ["success", "error"], "title": "Status", "type": "string"}}, "required": ["content", "tool_call_id"], "title": "ToolMessageChunk", "type": "object"}, "UsageMetadata": {"additionalProperties": true, "description": "Usage metadata for a message, such as token counts.\\n\\nThis is a standard representation of token usage that is consistent across models.\\n\\nExample:\\n ```python\\n {\\n \\"input_tokens\\": 350,\\n \\"output_tokens\\": 240,\\n \\"total_tokens\\": 590,\\n \\"input_token_details\\": {\\n \\"audio\\": 10,\\n \\"cache_creation\\": 200,\\n \\"cache_read\\": 100,\\n },\\n \\"output_token_details\\": {\\n \\"audio\\": 10,\\n \\"reasoning\\": 200,\\n },\\n }\\n ```\\n\\n!!! warning \\"Behavior changed in `langchain-core` 0.3.9\\"\\n\\n Added `input_token_details` and `output_token_details`.\\n\\n!!! note \\"LangSmith SDK\\"\\n\\n The LangSmith SDK also has a `UsageMetadata` class. While the two share fields,\\n LangSmith\'s `UsageMetadata` has additional fields to capture cost information\\n used by the LangSmith platform.", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}, "input_token_details": {"$ref": "#/$defs/InputTokenDetails"}, "output_token_details": {"$ref": "#/$defs/OutputTokenDetails"}}, "required": ["input_tokens", "output_tokens", "total_tokens"], "title": "UsageMetadata", "type": "object"}}, "default": null, "items": {"oneOf": [{"$ref": "#/$defs/AIMessage"}, {"$ref": "#/$defs/HumanMessage"}, {"$ref": "#/$defs/ChatMessage"}, {"$ref": "#/$defs/SystemMessage"}, {"$ref": "#/$defs/FunctionMessage"}, {"$ref": "#/$defs/ToolMessage"}, {"$ref": "#/$defs/AIMessageChunk"}, {"$ref": "#/$defs/HumanMessageChunk"}, {"$ref": "#/$defs/ChatMessageChunk"}, {"$ref": "#/$defs/SystemMessageChunk"}, {"$ref": "#/$defs/FunctionMessageChunk"}, {"$ref": "#/$defs/ToolMessageChunk"}]}, "title": "LangGraphOutput", "type": "array"}'
+# ---
+# name: test_message_graph[memory].2
+ '''
+ {
+ "nodes": [
+ {
+ "id": "__start__",
+ "type": "runnable",
+ "data": {
+ "id": [
+ "langgraph",
+ "_internal",
+ "_runnable",
+ "RunnableCallable"
+ ],
+ "name": "__start__"
+ }
+ },
+ {
+ "id": "agent",
+ "type": "runnable",
+ "data": {
+ "id": [
+ "tests",
+ "test_large_cases",
+ "FakeFunctionChatModel"
+ ],
+ "name": "agent"
+ }
+ },
+ {
+ "id": "tools",
+ "type": "runnable",
+ "data": {
+ "id": [
+ "langgraph",
+ "prebuilt",
+ "tool_node",
+ "ToolNode"
+ ],
+ "name": "tools"
+ }
+ },
+ {
+ "id": "__end__"
+ }
+ ],
+ "edges": [
+ {
+ "source": "__start__",
+ "target": "agent"
+ },
+ {
+ "source": "agent",
+ "target": "__end__",
+ "data": "end",
+ "conditional": true
+ },
+ {
+ "source": "agent",
+ "target": "tools",
+ "data": "continue",
+ "conditional": true
+ },
+ {
+ "source": "tools",
+ "target": "agent"
+ }
+ ]
+ }
+ '''
+# ---
+# name: test_message_graph[memory].3
+ '''
+ graph TD;
+ __start__ --> agent;
+ agent -. end .-> __end__;
+ agent -. continue .-> tools;
+ tools --> agent;
+
+ '''
+# ---
+# name: test_prebuilt_tool_chat
+ '{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of a chat model.\\n\\nExamples include [`HumanMessage`][langchain.messages.HumanMessage],\\n[`AIMessage`][langchain.messages.AIMessage], and\\n[`SystemMessage`][langchain.messages.SystemMessage].", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}}, "deprecated": true, "description": "The state of the agent.", "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}, "remaining_steps": {"title": "Remaining Steps", "type": "integer"}}, "required": ["messages"], "title": "AgentState", "type": "object"}'
+# ---
+# name: test_prebuilt_tool_chat.1
+ '{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of a chat model.\\n\\nExamples include [`HumanMessage`][langchain.messages.HumanMessage],\\n[`AIMessage`][langchain.messages.AIMessage], and\\n[`SystemMessage`][langchain.messages.SystemMessage].", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"additionalProperties": true, "title": "Additional Kwargs", "type": "object"}, "response_metadata": {"additionalProperties": true, "title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}}, "deprecated": true, "description": "The state of the agent.", "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}, "remaining_steps": {"title": "Remaining Steps", "type": "integer"}}, "required": ["messages"], "title": "AgentState", "type": "object"}'
+# ---
+# name: test_prebuilt_tool_chat.2
+ '''
+ {
+ "nodes": [
+ {
+ "id": "__start__",
+ "type": "runnable",
+ "data": {
+ "id": [
+ "langgraph",
+ "_internal",
+ "_runnable",
+ "RunnableCallable"
+ ],
+ "name": "__start__"
+ }
+ },
+ {
+ "id": "agent",
+ "type": "runnable",
+ "data": {
+ "id": [
+ "langgraph",
+ "_internal",
+ "_runnable",
+ "RunnableCallable"
+ ],
+ "name": "agent"
+ }
+ },
+ {
+ "id": "tools",
+ "type": "runnable",
+ "data": {
+ "id": [
+ "langgraph",
+ "prebuilt",
+ "tool_node",
+ "ToolNode"
+ ],
+ "name": "tools"
+ }
+ },
+ {
+ "id": "__end__"
+ }
+ ],
+ "edges": [
+ {
+ "source": "__start__",
+ "target": "agent"
+ },
+ {
+ "source": "agent",
+ "target": "__end__",
+ "conditional": true
+ },
+ {
+ "source": "agent",
+ "target": "tools",
+ "conditional": true
+ },
+ {
+ "source": "tools",
+ "target": "agent"
+ }
+ ]
+ }
+ '''
+# ---
+# name: test_prebuilt_tool_chat.3
+ '''
+ graph TD;
+ __start__ --> agent;
+ agent -.-> __end__;
+ agent -.-> tools;
+ tools --> agent;
+
+ '''
+# ---
+# name: test_send_react_interrupt_control[memory]
+ '''
+ ---
+ config:
+ flowchart:
+ curve: linear
+ ---
+ graph TD;
+ __start__([