Compare commits

..
Author SHA1 Message Date
Sydney Runkle 938d812089 try adding python bound 2025-06-08 10:22:16 -04:00
Sydney Runkle 95a2edc331 format docs 2025-06-08 10:09:25 -04:00
Sydney Runkle 0ed8d2007f linting 2025-06-08 10:03:46 -04:00
Sydney RunkleandGitHub aa9f562828 Merge branch 'main' into wfh/tavilyp2 2025-06-08 10:03:14 -04:00
William Fu-Hinthorn 8aa8ffcef5 Tavily in cli examples to own package 2025-03-31 16:17:23 -07:00
89 changed files with 7776 additions and 10474 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
name: "\U0001F41B Bug Report"
description: Report a bug in LangGraph. To report a security issue, please instead use the security option below. For questions, please use the GitHub Discussions.
labels: [pending,bug]
labels: ["02 Bug Report"]
body:
- type: markdown
attributes:
+1 -1
View File
@@ -1,4 +1,4 @@
blank_issues_enabled: true
blank_issues_enabled: false
version: 2.1
contact_links:
- name: 🤔 Question or Problem
+1 -1
View File
@@ -1,7 +1,7 @@
name: Documentation
description: Report an issue related to the LangGraph documentation.
title: "DOC: <Please write a comprehensive title after the 'DOC: ' prefix>"
labels: [documentation]
labels: [03 - Documentation]
body:
- type: textarea
-55
View File
@@ -1,55 +0,0 @@
# AGENTS Instructions
This repository is a monorepo. Each library lives in a subdirectory under `libs/`.
When you modify code in any library, run the following commands in that library's directory before creating a pull request:
- `make format` run code formatters
- `make lint` run the linter
- `make test` execute the test suite
To run a particular test file or to pass additional pytest options you can specify the `TEST` variable:
```
TEST=path/to/test.py make test
```
Other pytest arguments can also be supplied inside the `TEST` variable.
## Libraries
The repository contains several Python and JavaScript/TypeScript libraries.
Below is a high-level overview:
- **checkpoint** base interfaces for LangGraph checkpointers.
- **checkpoint-postgres** Postgres implementation of the checkpoint saver.
- **checkpoint-sqlite** SQLite implementation of the checkpoint saver.
- **cli** official command-line interface for LangGraph.
- **langgraph** core framework for building stateful, multi-actor agents.
- **prebuilt** high-level APIs for creating and running agents and tools.
- **sdk-js** JS/TS SDK for interacting with the LangGraph REST API.
- **sdk-py** Python SDK for the LangGraph Platform API.
### Dependency map
The diagram below lists downstream libraries for each production dependency as
declared in that library's `pyproject.toml` (or `package.json`).
```text
checkpoint
├── checkpoint-postgres
├── checkpoint-sqlite
├── prebuilt
└── langgraph
prebuilt
└── langgraph
sdk-py
├── langgraph
└── cli
sdk-js (standalone)
```
Changes to a library may impact all of its dependents shown above.
+1
View File
@@ -12,6 +12,7 @@
[![Downloads](https://static.pepy.tech/badge/langgraph/month)](https://pepy.tech/project/langgraph)
[![Open Issues](https://img.shields.io/github/issues-raw/langchain-ai/langgraph)](https://github.com/langchain-ai/langgraph/issues)
[![Docs](https://img.shields.io/badge/docs-latest-blue)](https://langchain-ai.github.io/langgraph/)
[![GitMCP](https://img.shields.io/endpoint?url=https://gitmcp.io/badge/langchain-ai/langgraph)](https://gitmcp.io/langchain-ai/langgraph)
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.
+122 -119
View File
@@ -1,154 +1,157 @@
"""Translate Python markdown to TypeScript and/or consolidate Python-JS markdown into a single document."""
"""Add typescript translation to a given markdown file."""
import argparse
import re
import requests
from langchain_anthropic import ChatAnthropic
# Load reference TypeScript snippets
URL = "https://gist.githubusercontent.com/eyurtsev/e7486731415463a9bc5b4682358859c8/raw/b5a5fda9c7e3387cfcb781f25082814d43675d50/gistfile1.txt"
response = requests.get(URL)
response.raise_for_status()
reference_snippets = response.text
# Initialize model
model = ChatAnthropic(model="claude-sonnet-4-0", max_tokens=64_000)
TRANSLATION_PROMPT = (
"You are a helpful assistant that translates Python-based technical "
"documentation written in Markdown to equivalent TypeScript-based documentation. "
"The input is a Markdown file written in mkdocs format. It contains "
"Python code snippets embedded in prose. "
"Your task is to rewrite the content by translating the Python code to "
"idiomatic TypeScript, using the provided TypeScript reference snippets "
"to ensure accurate and consistent usage (e.g., correct imports, function "
"names, and patterns). "
"Remove the original Python code and replace it with the corresponding "
"TypeScript version. "
"Do not alter the surrounding prose unless a change is necessary to "
"reflect differences between Python and TypeScript. "
"Preserve the structure and formatting of the original Markdown document. "
"Do not make stylistic or structural changes unless they directly support "
"the translation. "
"Use the reference TypeScript snippets as guidance whenever possible to "
"maintain alignment with existing conventions.\n\n"
f"Here are the reference TypeScript snippets:\n\n{reference_snippets}\n\n"
)
CONSOLIDATION_PROMPT = (
"You are a helpful assistant that consolidates parallel Python and JavaScript (TypeScript) technical documentation "
"written in Markdown into a single unified Markdown document. "
"The input consists of two documents: the first is for Python users, and the second is for JavaScript/TypeScript users. "
"Your task is to merge these into one Markdown file using language-specific fenced blocks to separate the content where needed. "
"Use the following syntax to distinguish content for each language:\n\n"
":::python\n"
"# Python-specific content\n"
":::\n\n"
":::js\n"
"# JavaScript/TypeScript-specific content\n"
":::\n\n"
"Follow these consolidation rules:\n"
"- When content (prose or code) is the same or nearly identical in both versions, include it only once—outside of any fenced block.\n"
"- When content differs between the Python and JS versions, wrap each version in its corresponding fenced block.\n"
"- Prefer **paragraph-level separation** of language-specific content. Do not combine Python and JS snippets or terminology in the same sentence or paragraph using conditional phrases.\n"
" For example, avoid inline constructs like:\n"
" `The :::python add_messages ::: :::js reducer ::: function...`\n"
" Instead, write two distinct paragraphs:\n\n"
" :::python\n"
" The `add_messages` function in our `State` will append the LLM's response messages to whatever messages are already in the state.\n"
" ::: \n\n"
" :::js\n"
" The `reducer` function in our `StateAnnotation` will append the LLM's response messages to whatever messages are already in the state.\n"
" :::\n\n"
"- Preserve the overall structure, ordering, and formatting of the original Markdown documents.\n"
"- Do not rephrase or unify content unless it is logically and semantically identical.\n"
"- Use the fenced blocks for both prose and code as needed, and ensure output is clean, readable Markdown suitable for tools that parse these directives.\n"
"Your goal is to produce a cleanly merged documentation file that serves both Python and JavaScript users without redundancy, while maximizing clarity and separation of language-specific details."
)
model = ChatAnthropic(model="claude-3-5-sonnet-latest")
def translate_python_to_ts(markdown_content: str) -> str:
response = model.invoke(
def _get_tqdm():
try:
from tqdm import tqdm
except ImportError:
# If not available return a simple identity function
def tqdm(iterable, *args, **kwargs):
return iterable
return tqdm
_tqdm = _get_tqdm()
opening_pattern = re.compile(r"^\s*```python(?:\s+.*)?\s*$")
closing_pattern = re.compile(r"^\s*```\s*$")
def extract_python_snippets(markdown: str) -> list[str]:
"""
Extract all python code blocks (including their fence lines) from the markdown content.
A python block is defined as any block that starts with a line containing an opening fence
with '```python' (optionally with extra parameters) and ends with a closing fence '```'.
"""
snippets = []
inside_block = False
current_snippet = []
for line in markdown.splitlines(keepends=True):
if not inside_block:
if opening_pattern.match(line):
inside_block = True
current_snippet = [line]
else:
current_snippet.append(line)
if closing_pattern.match(line):
inside_block = False
snippets.append("".join(current_snippet))
current_snippet = []
return snippets
def translate_snippet(python_snippet: str) -> str:
"""Translate a python code block into a TypeScript code block using Langchain.
The response is expected to be a properly fenced TypeScript code block (i.e.
starting with ```typescript and ending with ```).
"""
ai_message = model.invoke(
[
{
"role": "system",
"content": TRANSLATION_PROMPT,
"cache_control": {"type": "ephemeral"},
"content": (
f"You have access to the following up-to-date example TypeScript code "
f"snippets that show examples of building with langgraph "
f"and langchain:\n\n{reference_snippets}\n\n"
"Use this context to translate the following Python code to equivalent "
"TypeScript. Ensure that your output is a valid fenced TypeScript "
"code block (i.e. starts with ```typescript and ends with ```)."
),
},
{"role": "user", "content": markdown_content},
]
)
return response.content
def consolidate_python_and_ts(combined_content: str) -> str:
response = model.invoke(
[
{
"role": "system",
"content": CONSOLIDATION_PROMPT,
"cache_control": {"type": "ephemeral"},
"role": "user",
"content": f"Translate this Python snippet to TypeScript:\n\n{python_snippet}",
},
{"role": "user", "content": combined_content},
]
)
return response.content
# Use a regular expression to search for a TypeScript code block in the response.
pattern = r"```typescript\s*(.*?)\s*```"
match = re.search(pattern, ai_message.content, re.DOTALL)
if match:
# Reconstruct the code block with proper fences.
typescript_code = match.group(1).strip()
return f"```typescript\n{typescript_code}\n```"
else:
raise ValueError("No TypeScript code block found in the model's response.")
def main(file_path: str, translate_only: bool, consolidate_only: bool) -> None:
with open(file_path, "r", encoding="utf-8") as f:
def insert_translations_into_markdown(
markdown: str, typescript_snippets: list[str]
) -> str:
"""Walks through the original markdown content and, after each
Python snippet block, inserts the corresponding translated TypeScript snippet.
It assumes that the ordering of the Python snippets
(from extract_python_snippets) matches the order they appear in the markdown.
"""
output_lines = []
lines = markdown.splitlines(keepends=True)
inside_block = False
snippet_index = 0
for line in lines:
output_lines.append(line)
if not inside_block and opening_pattern.match(line):
# We've encountered the start of a python code block.
inside_block = True
elif inside_block:
if closing_pattern.match(line):
# End of a python snippet block.
inside_block = False
if snippet_index < len(typescript_snippets):
# Insert an extra newline for clarity, then the translated TypeScript snippet.
output_lines.append("\n")
output_lines.append(typescript_snippets[snippet_index])
output_lines.append("\n")
snippet_index += 1
return "".join(output_lines)
def main(file_path: str) -> None:
# Read the markdown file.
with open(file_path, "r") as f:
markdown_content = f.read()
if translate_only:
translated = translate_python_to_ts(markdown_content)
output_path = file_path.replace(".md", ".translated.md")
with open(output_path, "w", encoding="utf-8") as f:
f.write(translated)
print(f"Translated JS/TS version written to: {output_path}")
# 1. Extract all Python snippets.
python_snippets = extract_python_snippets(markdown_content)[:1]
elif consolidate_only:
consolidated = consolidate_python_and_ts(markdown_content)
with open(file_path, "w", encoding="utf-8") as f:
f.write(consolidated)
print(f"Consolidated content written to: {file_path}")
# 2. Translate each Python snippet to TypeScript.
typescript_snippets = []
# Replace with .batch() for faster translation
for python_snippet in _tqdm(python_snippets):
ts_snippet = translate_snippet(python_snippet)
typescript_snippets.append(ts_snippet)
else:
# Default behavior: translate first, then consolidate both
translated = translate_python_to_ts(markdown_content)
combined = f"{markdown_content.strip()}\n\n\n{translated.strip()}"
consolidated = consolidate_python_and_ts(combined)
with open(file_path, "w", encoding="utf-8") as f:
f.write(consolidated)
print(f"Translated and consolidated content written to: {file_path}")
# 3. Insert the TypeScript translations after their respective Python snippets.
updated_markdown = insert_translations_into_markdown(
markdown_content, typescript_snippets
)
# Overwrite the original markdown file with the updated content.
with open(file_path, "w") as f:
f.write(updated_markdown)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description=(
"Translate Python markdown to TypeScript and/or consolidate "
"Python-JS markdown into one file."
)
description="Translate Python snippets in a markdown file to TypeScript and insert them after each Python snippet."
)
parser.add_argument("file_path", type=str, help="Path to the markdown file.")
parser.add_argument(
"--translate-only",
action="store_true",
help="Only generate the JS translation.",
)
parser.add_argument(
"--consolidate-only",
action="store_true",
help="Only consolidate pre-paired Python and JS content.",
)
args = parser.parse_args()
if args.translate_only and args.consolidate_only:
raise ValueError(
"Cannot use both --translate-only and --consolidate-only at the same time."
)
main(
args.file_path,
translate_only=args.translate_only,
consolidate_only=args.consolidate_only,
)
main(args.file_path)
+6 -10
View File
@@ -3,21 +3,19 @@
import asyncio
import glob
import os
import re
from typing import TypedDict, List, Optional
import pydantic
import re
from pydantic import BaseModel, Field
from langchain_core.rate_limiters import InMemoryRateLimiter
import yaml
from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
from mkdocs.structure.files import File
from mkdocs.structure.pages import Page
from pydantic import BaseModel, Field
from yaml import SafeLoader
from _scripts.notebook_hooks import (
_on_page_markdown_with_config,
_apply_conditional_rendering,
)
from _scripts.notebook_hooks import _on_page_markdown_with_config
HERE = os.path.dirname(os.path.abspath(__file__))
# Get source directory (parent of HERE / docs)
@@ -213,9 +211,7 @@ async def process_nav_items(nav_items: list[NavItem]) -> list[NavItem]:
# Remove any items that start with http:// or https:// looking only for
# local file at this stages.
nav_items = [
item
for item in nav_items
if not item["url"].startswith(("http://", "https://"))
item for item in nav_items if not item["url"].startswith(("http://", "https://"))
]
# Process items in parallel
tasks = [process_single_item(item) for item in nav_items]
-5
View File
@@ -1,5 +0,0 @@
JS_LINK_MAP = {
"langgraph.types.interrupt": "https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph.interrupt-2.html",
"create_react_agent": "https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.createReactAgent.html",
"langgraph.types.Command": "https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph.Command.html",
}
-77
View File
@@ -15,7 +15,6 @@ from mkdocs.structure.files import Files, File
from mkdocs.structure.pages import Page
from _scripts.generate_api_reference_links import update_markdown_with_imports
from _scripts.link_map import JS_LINK_MAP
from _scripts.notebook_convert import convert_notebook
logger = logging.getLogger(__name__)
@@ -159,68 +158,6 @@ def _add_path_to_code_blocks(markdown: str, page: Page) -> str:
return code_block_pattern.sub(replace_code_block_header, markdown)
def _resolve_cross_references(md_text: str, link_map: dict[str, str]) -> str:
"""Replace [title][identifier] with [title](url) using language-specific link_map.
Args:
md_text: The markdown text to process.
link_map: mapping of identifier to URL.
Returns:
The processed markdown text with cross-references resolved.
"""
# Pattern to match [title][identifier]
pattern = re.compile(r"\[([^\]]+)\]\[([^\]]+)\]")
def replace_reference(match: re.Match) -> str:
"""Replace the matched reference with the corresponding URL."""
title, identifier = match.group(1), match.group(2)
url = link_map.get(identifier)
if url:
return f"[{title}]({url})"
else:
# Leave it unchanged if not found
return match.group(0)
return pattern.sub(replace_reference, md_text)
def _apply_conditional_rendering(md_text: str, target_language: str) -> str:
if target_language not in {"python", "js", "switcher"}:
raise ValueError("target_language must be 'python' or 'js'")
pattern = re.compile(
r"(?P<indent>[ \t]*):::(?P<language>\w+)\s*\n"
r"(?P<content>((?:.*\n)*?))" # Capture the content inside the block
r"(?P=indent):::" # Match closing with the same indentation
)
def replace_conditional_blocks(match: re.Match) -> str:
"""Keep active conditionals."""
language = match.group("language")
content = match.group("content")
if language not in {"python", "js", "switcher"}:
# If the language is not supported, return the original block
return match.group(0)
if target_language == "switcher":
# Both Python and JavaScript blocks are wrapped in a tag that
# allows the user to switch between them.
standardized_language = "javascript" if language == "js" else "python"
return f'<div class="lang-{standardized_language}">\n' + content + "\n</div>"
if language == target_language:
return content
# If the language does not match, return an empty string
return ""
processed = pattern.sub(replace_conditional_blocks, md_text)
return processed
def _highlight_code_blocks(markdown: str) -> str:
"""Find code blocks with highlight comments and add hl_lines attribute.
@@ -320,20 +257,6 @@ def _on_page_markdown_with_config(
# Apply highlight comments to code blocks
markdown = _highlight_code_blocks(markdown)
# Apply conditional rendering for code blocks
target_language = kwargs.get("target_language", "js")
markdown = _apply_conditional_rendering(markdown, "switcher")
if target_language == "js":
markdown = _resolve_cross_references(markdown, JS_LINK_MAP)
elif target_language == "python":
# Via a dedicated plugin
pass
else:
raise ValueError(
f"Unsupported target language: {target_language}. "
"Supported languages are 'python' and 'js'."
)
# Add file path as an attribute to code blocks that are executable.
# This file path is used to associate fixtures with the executable code
# which can be used in CI to test the docs without making network requests.
+1 -1
View File
@@ -233,4 +233,4 @@ Tools can access context through special parameter **annotations**.
### Update Context from Tools
Tools can update agent's context (state and long-term memory) during execution. This is useful for persisting intermediate results or making information accessible to subsequent tools or prompts. See [Memory](./memory.md#read-short-term) guide for more information.
Tools can update agent's context (state and long-term memory) during execution. This is useful for persisting intermediate results or making information accessible to subsequent tools or prompts. See [Memory](./memory.md#read-short-term) guide for more information.
+1 -1
View File
@@ -106,4 +106,4 @@ if __name__ == "__main__":
## Additional resources
- [MCP documentation](https://modelcontextprotocol.io/introduction)
- [MCP Transport documentation](https://modelcontextprotocol.io/docs/concepts/transports)
- [MCP Transport documentation](https://modelcontextprotocol.io/docs/concepts/transports)
@@ -30,16 +30,18 @@ Before deploying, review the [conceptual guide for the Self-Hosted Control Plane
1. `LangGraphPlatform CRD`: A CRD for LangGraph Platform deployments. This contains the spec for managing an instance of a LangGraph platform deployment.
1. `operator`: This operator handles changes to your LangGraph Platform CRDs.
1. `host-backend`: This is the [control plane](../../concepts/langgraph_control_plane.md).
1. Two additional images will be used by the chart. Use the images that are specified in the latest release.
1. Two additional images will be used by the chart.
hostBackendImage:
repository: "docker.io/langchain/hosted-langserve-backend"
pullPolicy: IfNotPresent
tag: "0.9.80"
operatorImage:
repository: "docker.io/langchain/langgraph-operator"
pullPolicy: IfNotPresent
tag: "aa9dff4"
1. In your config file for langsmith (usually `langsmith_config.yaml`, enable the `langgraphPlatform` option. Note that you must also have a valid ingress setup:
1. In your `langsmith_config.yaml` file, enable the `langgraphPlatform` option. Note that you must also have a valid ingress setup:
config:
langgraphPlatform:
@@ -3818,14 +3818,6 @@
"title": "Filter",
"description": "Optional dictionary of key-value pairs to filter results."
},
"query": {
"type": [
"string",
"null"
],
"title": "Query",
"description": "Query string for semantic/vector search."
},
"limit": {
"type": "integer",
"default": 10,
-9
View File
@@ -123,12 +123,3 @@ Defaults to `''`.
Set `REDIS_CLUSTER` to `True` to enable Redis Cluster mode. When enabled, the system will connect to Redis using cluster mode. This is useful when connecting to a Redis Cluster deployment.
Defaults to `False`.
## `MOUNT_PREFIX`
!!! info "Only Allowed in Self-Hosted Deployments"
The `MOUNT_PREFIX` environment variable is only allowed in Self-Hosted Deployment models, LangGraph Platform SaaS will not allow this environment variable.
Set `MOUNT_PREFIX` to serve the LangGraph Server under a specific path prefix. This is useful for deployments where the server is behind a reverse proxy or load balancer that requires a specific path prefix.
For example, if the server is to be served under `https://example.com/langgraph`, set `MOUNT_PREFIX` to `/langgraph`.
+1 -43
View File
@@ -470,51 +470,9 @@ If the checkpointer is used with asynchronous graph execution (i.e. executing th
### Serializer
When checkpointers save the graph state, they need to serialize the channel values in the state. This is done using serializer objects.
When checkpointers save the graph state, they need to serialize the channel values in the state. This is done using serializer objects.
`langgraph_checkpoint` defines [protocol][langgraph.checkpoint.serde.base.SerializerProtocol] for implementing serializers provides a default implementation ([JsonPlusSerializer][langgraph.checkpoint.serde.jsonplus.JsonPlusSerializer]) that handles a wide variety of types, including LangChain and LangGraph primitives, datetimes, enums and more.
#### Serialization with `pickle`
The default serializer, [`JsonPlusSerializer`][langgraph.checkpoint.serde.jsonplus.JsonPlusSerializer], uses ormsgpack and JSON under the hood, which is not suitable for all types of objects.
If you want to fallback to pickle for objects not currently supported by our msgpack encoder (such as Pandas dataframes),
you can use the `pickle_fallback` argument of the `JsonPlusSerializer`:
```python
from langgraph.checkpoint.memory import MemorySaver
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
# ... Define the graph ...
graph.compile(
checkpointer=MemorySaver(serde=JsonPlusSerializer(pickle_fallback=True))
)
```
#### Encryption
Checkpointers can optionally encrypt all persisted state. To enable this, pass an instance of [`EncryptedSerializer`][langgraph.checkpoint.serde.encrypted.EncryptedSerializer] to the `serde` argument of any `BaseCheckpointSaver` implementation. The easiest way to create an encrypted serializer is via [`from_pycryptodome_aes`][langgraph.checkpoint.serde.encrypted.EncryptedSerializer.from_pycryptodome_aes], which reads the AES key from the `LANGGRAPH_AES_KEY` environment variable (or accepts a `key` argument):
```python
import sqlite3
from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
from langgraph.checkpoint.sqlite import SqliteSaver
serde = EncryptedSerializer.from_pycryptodome_aes() # reads LANGGRAPH_AES_KEY
checkpointer = SqliteSaver(sqlite3.connect("checkpoint.db"), serde=serde)
```
```python
from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
from langgraph.checkpoint.postgres import PostgresSaver
serde = EncryptedSerializer.from_pycryptodome_aes()
checkpointer = PostgresSaver.from_conn_string("postgresql://...", serde=serde)
checkpointer.setup()
```
When running on LangGraph Platform, encryption is automatically enabled whenever `LANGGRAPH_AES_KEY` is present, so you only need to provide the environment variable. Other encryption schemes can be used by implementing [`CipherProtocol`][langgraph.checkpoint.serde.base.CipherProtocol] and supplying it to `EncryptedSerializer`.
## Capabilities
### Human-in-the-loop
+2 -3
View File
@@ -59,9 +59,8 @@ The main question when adding subgraphs is how the parent graph and subgraph com
response = model.invoke(state["subgraph_messages"])
return {"subgraph_messages": response}
subgraph_builder = StateGraph(SubgraphMessagesState)
subgraph_builder.add_node("call_model_from_subgraph", call_model)
subgraph_builder.add_edge(START, "call_model_from_subgraph")
subgraph_builder = StateGraph(State)
subgraph_builder.add_node(call_model)
...
# highlight-next-line
subgraph = subgraph_builder.compile()
+2 -2
View File
@@ -1107,10 +1107,10 @@
"source": [
"### Use in production\n",
"\n",
"In production, you would want to use a store backed by a database:\n",
"In production, you would want to use a checkpointer backed by a database:\n",
"\n",
"```python\n",
"from langgraph.store.postgres import PostgresStore\n",
"from langgraph.checkpoint.postgres import PostgresSaver\n",
"\n",
"DB_URI = \"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable\"\n",
"# highlight-next-line\n",
+1 -7
View File
@@ -12,18 +12,12 @@
options:
members:
- SerializerProtocol
- CipherProtocol
::: langgraph.checkpoint.serde.jsonplus
options:
members:
- JsonPlusSerializer
::: langgraph.checkpoint.serde.encrypted
options:
members:
- EncryptedSerializer
::: langgraph.checkpoint.memory
::: langgraph.checkpoint.sqlite
@@ -38,4 +32,4 @@
::: langgraph.checkpoint.postgres.aio
options:
members:
- AsyncPostgresSaver
- AsyncPostgresSaver
@@ -580,7 +580,9 @@
" ]\n",
")\n",
"\n",
"evaluator = prompt | ChatOpenAI(model=\"gpt-4o\").with_structured_output(RedTeamingResult)\n",
"evaluator = prompt | ChatOpenAI(model=\"gpt-4-turbo-preview\").with_structured_output(\n",
" RedTeamingResult, method=\"function_calling\"\n",
")\n",
"\n",
"\n",
"def did_resist(run, example):\n",
@@ -1,6 +1,6 @@
# Build a basic chatbot
In this tutorial, you will build a basic chatbot. This chatbot is the basis for the following series of tutorials where you will progressively add more sophisticated capabilities, and be introduced to key LangGraph concepts along the way. Let's dive in! 🌟
In this tutorial, you will build a basic chatbot. This chatbot is the basis for the following series of tutorials where you will progressively add more sophisticated capabilities, and be introduced to key LangGraph concepts along the way. Lets dive in! 🌟
## Prerequisites
@@ -13,17 +13,9 @@ tool-calling features, such as [OpenAI](https://platform.openai.com/api-keys),
Install the required packages:
:::python
```bash
pip install -U langgraph langsmith
```
:::
:::js
```bash
npm install @langchain/langgraph @langchain/core langsmith
```
:::
!!! tip
@@ -35,7 +27,6 @@ Now you can create a basic chatbot using LangGraph. This chatbot will respond di
Start by creating a `StateGraph`. A `StateGraph` object defines the structure of our chatbot as a "state machine". We'll add `nodes` to represent the llm and functions our chatbot can call and `edges` to specify how the bot should transition between these functions.
:::python
```python
from typing import Annotated
@@ -54,53 +45,24 @@ class State(TypedDict):
graph_builder = StateGraph(State)
```
:::
:::js
```typescript
import { Annotation } from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";
import { StateGraph, START, END } from "@langchain/langgraph";
const StateAnnotation = Annotation.Root({
// Messages have the type "BaseMessage[]". The messagesStateReducer function
// defines how this state key should be updated
// (in this case, it appends messages to the list, rather than overwriting them)
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
});
const graphBuilder = new StateGraph(StateAnnotation);
```
:::
Our graph can now handle two key tasks:
1. Each `node` can receive the current `State` as input and output an update to the state.
2. Updates to `messages` will be appended to the existing list rather than overwriting it, thanks to the prebuilt function used with the annotation.
2. Updates to `messages` will be appended to the existing list rather than overwriting it, thanks to the prebuilt [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/?h=add+messages#add_messages) function used with the `Annotated` syntax.
------
!!! tip "Concept"
When defining a graph, the first step is to define its `State`. The `State` includes the graph's schema and [reducer functions](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) that handle state updates. Keys without a reducer annotation will overwrite previous values. To learn more about state, reducers, and related concepts, see [LangGraph reference docs](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages).
:::python
In our example, `State` is a `TypedDict` with one key: `messages`. The [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages) reducer function is used to append new messages to the list instead of overwriting it.
:::
:::js
In our example, `StateAnnotation` defines a state with one key: `messages`. The reducer function is used to append new messages to the list instead of overwriting it.
:::
When defining a graph, the first step is to define its `State`. The `State` includes the graph's schema and [reducer functions](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) that handle state updates. In our example, `State` is a `TypedDict` with one key: `messages`. The [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages) reducer function is used to append new messages to the list instead of overwriting it. Keys without a reducer annotation will overwrite previous values. To learn more about state, reducers, and related concepts, see [LangGraph reference docs](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages).
## 3. Add a node
Next, add a "`chatbot`" node. **Nodes** represent units of work and are typically regular functions.
Next, add a "`chatbot`" node. **Nodes** represent units of work and are typically regular Python functions.
Let's first select a chat model:
:::python
{!snippets/chat_model_tabs.md!}
<!---
@@ -110,21 +72,10 @@ from langchain.chat_models import init_chat_model
llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
```
-->
:::
:::js
```typescript
import { ChatAnthropic } from "@langchain/anthropic";
const llm = new ChatAnthropic({
model: "claude-3-5-sonnet-latest",
});
```
:::
We can now incorporate the chat model into a simple node:
:::python
```python
def chatbot(state: State):
@@ -136,63 +87,26 @@ def chatbot(state: State):
# the node is used.
graph_builder.add_node("chatbot", chatbot)
```
:::
:::js
```typescript
const chatbot = async (state: typeof StateAnnotation.State) => {
return { messages: [await llm.invoke(state.messages)] };
};
// The first argument is the unique node name
// The second argument is the function or object that will be called whenever
// the node is used.
graphBuilder.addNode("chatbot", chatbot);
```
:::
**Notice** how the `chatbot` node function takes the current `State` as input and returns a dictionary containing an updated `messages` list under the key "messages". This is the basic pattern for all LangGraph node functions.
:::python
The `add_messages` function in our `State` will append the LLM's response messages to whatever messages are already in the state.
:::
:::js
The reducer function in our `StateAnnotation` will append the LLM's response messages to whatever messages are already in the state.
:::
## 4. Add an `entry` point
Add an `entry` point to tell the graph **where to start its work** each time it is run:
:::python
```python
graph_builder.add_edge(START, "chatbot")
```
:::
:::js
```typescript
graphBuilder.addEdge(START, "chatbot");
```
:::
## 5. Add an `exit` point
Add an `exit` point to indicate **where the graph should finish execution**. This is helpful for more complex flows, but even in a simple graph like this, adding an end node improves clarity.
:::python
```python
graph_builder.add_edge("chatbot", END)
```
:::
:::js
```typescript
graphBuilder.addEdge("chatbot", END);
```
:::
This tells the graph to terminate after running the chatbot node.
## 6. Compile the graph
@@ -200,23 +114,14 @@ This tells the graph to terminate after running the chatbot node.
Before running the graph, we'll need to compile it. We can do so by calling `compile()`
on the graph builder. This creates a `CompiledGraph` we can invoke on our state.
:::python
```python
graph = graph_builder.compile()
```
:::
:::js
```typescript
const graph = graphBuilder.compile();
```
:::
## 7. Visualize the graph (optional)
You can visualize the graph using the `get_graph` method and one of the "draw" methods, like `draw_ascii` or `draw_png`. The `draw` methods each require additional dependencies.
:::python
```python
from IPython.display import Image, display
@@ -226,31 +131,14 @@ except Exception:
# This requires some extra dependencies and is optional
pass
```
:::
:::js
```typescript
import * as tslab from "tslab";
try {
const drawableGraph = graph.getGraph();
const image = await drawableGraph.drawMermaidPng();
const arrayBuffer = await image.arrayBuffer();
await tslab.display.png(new Uint8Array(arrayBuffer));
} catch (error) {
// This requires some extra dependencies and is optional
console.log("Graph visualization not available");
}
```
:::
![basic chatbot diagram](basic-chatbot.png)
## 8. Run the chatbot
Now run the chatbot!
:::python
!!! tip
You can exit the chat loop at any time by typing `quit`, `exit`, or `q`.
@@ -281,41 +169,11 @@ while True:
Assistant: LangGraph is a library designed to help build stateful multi-agent applications using language models. It provides tools for creating workflows and state machines to coordinate multiple AI agents or language model interactions. LangGraph is built on top of LangChain, leveraging its components while adding graph-based coordination capabilities. It's particularly useful for developing more complex, stateful AI applications that go beyond simple query-response interactions.
Goodbye!
```
:::
:::js
```typescript
import { HumanMessage } from "@langchain/core/messages";
async function streamGraphUpdates(userInput: string) {
const stream = await graph.stream({
messages: [new HumanMessage(userInput)]
});
for await (const event of stream) {
for (const value of Object.values(event)) {
console.log("Assistant:", value.messages[value.messages.length - 1].content);
}
}
}
// Example usage
const userInput = "What do you know about LangGraph?";
console.log("User:", userInput);
await streamGraphUpdates(userInput);
```
```
User: What do you know about LangGraph?
Assistant: LangGraph is a library designed to help build stateful multi-agent applications using language models. It provides tools for creating workflows and state machines to coordinate multiple AI agents or language model interactions. LangGraph is built on top of LangChain, leveraging its components while adding graph-based coordination capabilities. It's particularly useful for developing more complex, stateful AI applications that go beyond simple query-response interactions.
```
:::
**Congratulations!** You've built your first chatbot using LangGraph. This bot can engage in basic conversation by taking user input and generating responses using an LLM. You can inspect a [LangSmith Trace](https://smith.langchain.com/public/7527e308-9502-4894-b347-f34385740d5a/r) for the call above.
Below is the full code for this tutorial:
:::python
```python
from typing import Annotated
@@ -348,41 +206,9 @@ graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
graph = graph_builder.compile()
```
:::
:::js
```typescript
import { Annotation } from "@langchain/langgraph";
import { BaseMessage, HumanMessage } from "@langchain/core/messages";
import { StateGraph, START, END } from "@langchain/langgraph";
import { ChatAnthropic } from "@langchain/anthropic";
const StateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
});
const graphBuilder = new StateGraph(StateAnnotation);
const llm = new ChatAnthropic({
model: "claude-3-5-sonnet-latest",
});
const chatbot = async (state: typeof StateAnnotation.State) => {
return { messages: [await llm.invoke(state.messages)] };
};
// The first argument is the unique node name
// The second argument is the function or object that will be called whenever
// the node is used.
graphBuilder.addNode("chatbot", chatbot);
graphBuilder.addEdge(START, "chatbot");
graphBuilder.addEdge("chatbot", END);
const graph = graphBuilder.compile();
```
:::
## Next steps
You may have noticed that the bot's knowledge is limited to what's in its training data. In the next part, we'll [add a web search tool](./2-add-tools.md) to expand the bot's knowledge and make it more capable.
You may have noticed that the bot's knowledge is limited to what's in its training data. In the next part, we'll [add a web search tool](./2-add-tools.md) to expand the bot's knowledge and make it more capable.
+1 -313
View File
@@ -8,39 +8,19 @@ To handle queries that your chatbot can't answer "from memory", integrate a web
## Prerequisites
:::python
Before you start this tutorial, ensure you have the following:
- An API key for the [Tavily Search Engine](https://python.langchain.com/docs/integrations/tools/tavily_search/).
:::
:::js
Before you start this tutorial, ensure you have the following:
- An API key for the [Tavily Search Engine](https://js.langchain.com/docs/integrations/tools/tavily_search/).
:::
## 1. Install the search engine
:::python
Install the requirements to use the [Tavily Search Engine](https://python.langchain.com/docs/integrations/tools/tavily_search/):
```bash
pip install -U langchain-tavily
```
:::
:::js
Install the requirements to use the [Tavily Search Engine](https://js.langchain.com/docs/integrations/tools/tavily_search/):
```bash
npm install @langchain/community
```
:::
## 2. Configure your environment
:::python
Configure your environment with your search engine API key:
```bash
@@ -50,21 +30,11 @@ _set_env("TAVILY_API_KEY")
```
TAVILY_API_KEY: ········
```
:::
:::js
Configure your environment with your search engine API key:
```typescript
process.env.TAVILY_API_KEY = "tvly-...";
```
:::
## 3. Define the tool
Define the web search tool:
:::python
```python
from langchain_tavily import TavilySearch
@@ -72,21 +42,9 @@ tool = TavilySearch(max_results=2)
tools = [tool]
tool.invoke("What's a 'node' in LangGraph?")
```
:::
:::js
```typescript
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
const tool = new TavilySearchResults({ maxResults: 2 });
const tools = [tool];
await tool.invoke("What's a 'node' in LangGraph?");
```
:::
The results are page summaries our chat bot can use to answer questions:
:::python
```
{'query': "What's a 'node' in LangGraph?",
'follow_up_questions': None,
@@ -104,17 +62,9 @@ The results are page summaries our chat bot can use to answer questions:
'raw_content': None}],
'response_time': 1.38}
```
:::
:::js
```
'[{"title":"Introduction to LangGraph: A Beginner\'s Guide - Medium","url":"https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141","content":"Stateful Graph: LangGraph revolves around the concept of a stateful graph, where each node in the graph represents a step in your computation, and the graph maintains a state that is passed around and updated as the computation progresses. LangGraph supports conditional edges, allowing you to dynamically determine the next node to execute based on the current state of the graph. We define nodes for classifying the input, handling greetings, and handling search queries. def classify_input_node(state): LangGraph is a versatile tool for building complex, stateful applications with LLMs. By understanding its core concepts and working through simple examples, beginners can start to leverage its power for their projects. Remember to pay attention to state management, conditional edges, and ensuring there are no dead-end nodes in your graph.","score":0.7065353,"raw_content":null},{"title":"LangGraph Tutorial: What Is LangGraph and How to Use It?","url":"https://www.datacamp.com/tutorial/langgraph-tutorial","content":"LangGraph is a library within the LangChain ecosystem that provides a framework for defining, coordinating, and executing multiple LLM agents (or chains) in a structured and efficient manner. By managing the flow of data and the sequence of operations, LangGraph allows developers to focus on the high-level logic of their applications rather than the intricacies of agent coordination. Whether you need a chatbot that can handle various types of user requests or a multi-agent system that performs complex tasks, LangGraph provides the tools to build exactly what you need. LangGraph significantly simplifies the development of complex LLM applications by providing a structured framework for managing state and coordinating agent interactions.","score":0.5008063,"raw_content":null}]'
```
:::
## 4. Define the graph
:::python
For the `StateGraph` you created in the [first tutorial](./1-build-basic-chatbot.md), add `bind_tools` on the LLM. This lets the LLM know the correct JSON format to use if it wants to use the search engine.
Let's first select our LLM:
@@ -153,52 +103,9 @@ def chatbot(state: State):
graph_builder.add_node("chatbot", chatbot)
```
:::
:::js
For the `StateGraph` you created in the [first tutorial](./1-build-basic-chatbot.md), add `bindTools` on the LLM. This lets the LLM know the correct JSON format to use if it wants to use the search engine.
Let's first select our LLM:
```typescript
import { ChatOpenAI } from "@langchain/openai";
const llm = new ChatOpenAI({
model: "gpt-4o",
temperature: 0,
});
```
We can now incorporate it into a `StateGraph`:
```typescript hl_lines="15"
import { Annotation } from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";
const StateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
});
import { StateGraph, START, END } from "@langchain/langgraph";
const graphBuilder = new StateGraph(StateAnnotation);
// Modification: tell the LLM which tools it can call
const llmWithTools = llm.bindTools(tools);
const chatbot = async (state: typeof StateAnnotation.State) => {
return { messages: [await llmWithTools.invoke(state.messages)] };
};
graphBuilder.addNode("chatbot", chatbot);
```
:::
## 5. Create a function to run the tools
:::python
Now, create a function to run the tools if they are called. Do this by adding the tools to a new node called`BasicToolNode` that checks the most recent message in the state and calls tools if the message contains `tool_calls`. It relies on the LLM's `tool_calling` support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers.
```python
@@ -236,50 +143,6 @@ class BasicToolNode:
tool_node = BasicToolNode(tools=[tool])
graph_builder.add_node("tools", tool_node)
```
:::
:::js
Now, create a function to run the tools if they are called. Do this by adding the tools to a new node called `BasicToolNode` that checks the most recent message in the state and calls tools if the message contains `tool_calls`. It relies on the LLM's `tool_calling` support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers.
```typescript
import { ToolMessage } from "@langchain/core/messages";
class BasicToolNode {
private toolsByName: Record<string, any>;
constructor(tools: any[]) {
this.toolsByName = {};
for (const tool of tools) {
this.toolsByName[tool.name] = tool;
}
}
async __call__(inputs: Record<string, any>): Promise<{ messages: ToolMessage[] }> {
const messages = inputs.messages || [];
if (messages.length === 0) {
throw new Error("No message found in input");
}
const message = messages[messages.length - 1];
const outputs: ToolMessage[] = [];
for (const toolCall of message.tool_calls || []) {
const toolResult = await this.toolsByName[toolCall.name].invoke(toolCall.args);
outputs.push(
new ToolMessage({
content: JSON.stringify(toolResult),
name: toolCall.name,
tool_call_id: toolCall.id,
})
);
}
return { messages: outputs };
}
}
const toolNode = new BasicToolNode([tool]);
graphBuilder.addNode("tools", async (state) => toolNode.__call__(state));
```
:::
!!! note
@@ -291,7 +154,6 @@ With the tool node added, now you can define the `conditional_edges`.
**Edges** route the control flow from one node to the next. **Conditional edges** start from a single node and usually contain "if" statements to route to different nodes depending on the current graph state. These functions receive the current graph `state` and return a string or list of strings indicating which node(s) to call next.
:::python
Next, define a router function called `route_tools` that checks for `tool_calls` in the chatbot's output. Provide this function to the graph by calling `add_conditional_edges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next.
The condition will route to `tools` if tool calls are present and `END` if not. Because the condition can return `END`, you do not need to explicitly set a `finish_point` this time.
@@ -332,51 +194,6 @@ graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge(START, "chatbot")
graph = graph_builder.compile()
```
:::
:::js
Next, define a router function called `routeTools` that checks for `tool_calls` in the chatbot's output. Provide this function to the graph by calling `addConditionalEdges`, which tells the graph that whenever the `chatbot` node completes to check this function to see where to go next.
The condition will route to `tools` if tool calls are present and `END` if not. Because the condition can return `END`, you do not need to explicitly set a `finish_point` this time.
```typescript
import { AIMessage } from "@langchain/core/messages";
const routeTools = (state: typeof StateAnnotation.State) => {
/**
* Use in the conditional_edge to route to the ToolNode if the last message
* has tool calls. Otherwise, route to the end.
*/
const messages = state.messages;
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls && lastMessage.tool_calls.length > 0) {
return "tools";
}
return END;
};
// The `routeTools` function returns "tools" if the chatbot asks to use a tool, and "END" if
// it is fine directly responding. This conditional routing defines the main agent loop.
graphBuilder.addConditionalEdges(
"chatbot",
routeTools,
// The following dictionary lets you tell the graph to interpret the condition's outputs as a specific node
// It defaults to the identity function, but if you
// want to use a node named something else apart from "tools",
// You can update the value of the dictionary to something else
// e.g., "tools": "my_tools"
{
tools: "tools",
[END]: END,
}
);
// Any time a tool is called, we return to the chatbot to decide the next step
graphBuilder.addEdge("tools", "chatbot");
graphBuilder.addEdge(START, "chatbot");
const graph = graphBuilder.compile();
```
:::
!!! note
@@ -384,7 +201,6 @@ const graph = graphBuilder.compile();
## 7. Visualize the graph (optional)
:::python
You can visualize the graph using the `get_graph` method and one of the "draw" methods, like `draw_ascii` or `draw_png`. The `draw` methods each require additional dependencies.
```python
@@ -396,26 +212,6 @@ except Exception:
# This requires some extra dependencies and is optional
pass
```
:::
:::js
You can visualize the graph using the `getGraph` method and one of the "draw" methods, like `drawAscii` or `drawMermaidPng`. The `draw` methods each require additional dependencies.
```typescript
import * as tslab from "tslab";
try {
const representation = graph.getGraph();
const image = await representation.drawMermaidPng();
const arrayBuffer = await image.arrayBuffer();
await tslab.display.png(new Uint8Array(arrayBuffer));
} catch (error) {
// This requires some extra dependencies and is optional
console.log("Graph visualization not available");
}
```
:::
![chatbot-with-tools-diagram](chatbot-with-tools.png)
@@ -423,7 +219,6 @@ try {
Now you can ask the chatbot questions outside its training data:
:::python
```python
def stream_graph_updates(user_input: str):
for event in graph.stream({"messages": [{"role": "user", "content": user_input}]}):
@@ -479,71 +274,11 @@ LangGraph appears to be a significant tool in the evolving landscape of LLM-base
Goodbye!
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
```
:::
:::js
```typescript
import { HumanMessage } from "@langchain/core/messages";
const streamGraphUpdates = async (userInput: string) => {
const stream = await graph.stream(
{ messages: [new HumanMessage(userInput)] },
{ streamMode: "values" }
);
for await (const event of stream) {
const messages = event.messages;
const lastMessage = messages[messages.length - 1];
console.log("Assistant:", lastMessage.content);
}
};
// Example usage
const userInput = "What do you know about LangGraph?";
console.log("User:", userInput);
await streamGraphUpdates(userInput);
```
```
Assistant: I'll search for information about LangGraph to provide you with accurate details.
Assistant: [{"title": "Introduction to LangGraph: A Beginner's Guide - Medium", "url": "https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141", "content": "Stateful Graph: LangGraph revolves around the concept of a stateful graph, where each node in the graph represents a step in your computation, and the graph maintains a state that is passed around and updated as the computation progresses. LangGraph supports conditional edges, allowing you to dynamically determine the next node to execute based on the current state of the graph. We define nodes for classifying the input, handling greetings, and handling search queries. def classify_input_node(state): LangGraph is a versatile tool for building complex, stateful applications with LLMs. By understanding its core concepts and working through simple examples, beginners can start to leverage its power for their projects. Remember to pay attention to state management, conditional edges, and ensuring there are no dead-end nodes in your graph.", "score": 0.7065353, "raw_content": null}, {"title": "LangGraph Tutorial: What Is LangGraph and How to Use It?", "url": "https://www.datacamp.com/tutorial/langgraph-tutorial", "content": "LangGraph is a library within the LangChain ecosystem that provides a framework for defining, coordinating, and executing multiple LLM agents or chains in a structured and efficient manner. By managing the flow of data and the sequence of operations, LangGraph allows developers to focus on the high-level logic of their applications rather than the intricacies of agent coordination. Whether you need a chatbot that can handle various types of user requests or a multi-agent system that performs complex tasks, LangGraph provides the tools to build exactly what you need. LangGraph significantly simplifies the development of complex LLM applications by providing a structured framework for managing state and coordinating agent interactions.", "score": 0.5008063, "raw_content": null}]
Assistant: Based on the search results, I can provide you with comprehensive information about LangGraph:
## What is LangGraph?
LangGraph is a library within the LangChain ecosystem designed for building stateful, multi-actor applications with Large Language Models (LLMs). It provides a framework for defining, coordinating, and executing multiple LLM agents or chains in a structured and efficient manner.
## Key Features:
1. **Stateful Graph Architecture**: LangGraph revolves around the concept of a stateful graph where each node represents a step in your computation, and the graph maintains state that is passed around and updated as the computation progresses.
2. **Conditional Edges**: It supports conditional edges, allowing you to dynamically determine the next node to execute based on the current state of the graph.
3. **Multi-Agent Coordination**: LangGraph manages the flow of data and sequence of operations, allowing developers to focus on high-level logic rather than the intricacies of agent coordination.
## Use Cases:
- Building conversational agents
- Creating chatbots that can handle various types of user requests
- Developing multi-agent systems that perform complex tasks
- Complex task automation
- Custom LLM-backed experiences
## Benefits:
- **Simplified Development**: LangGraph significantly simplifies the development of complex LLM applications by providing a structured framework for managing state and coordinating agent interactions.
- **Flexibility**: It's a versatile tool for building complex, stateful applications with LLMs.
- **Focus on Logic**: Developers can focus on the high-level logic of their applications rather than coordination details.
LangGraph is particularly valuable for projects that require sophisticated AI workflows with multiple steps, decision points, and state management across different components.
```
:::
## 9. Use prebuilts
For ease of use, adjust your code to replace the following with LangGraph prebuilt components. These have built in functionality like parallel API execution.
:::python
- `BasicToolNode` is replaced with the prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/prebuilt/#toolnode)
- `route_tools` is replaced with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition)
@@ -587,56 +322,9 @@ graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge(START, "chatbot")
graph = graph_builder.compile()
```
:::
:::js
- `BasicToolNode` is replaced with the prebuilt [ToolNode](https://langchain-ai.github.io/langgraph/reference/prebuilt/#toolnode)
- `routeTools` is replaced with the prebuilt [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#tools_condition)
```typescript hl_lines="25 30"
import { Annotation } from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
import { ChatOpenAI } from "@langchain/openai";
import { StateGraph, START, END } from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
const StateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
});
const graphBuilder = new StateGraph(StateAnnotation);
const tool = new TavilySearchResults({ maxResults: 2 });
const tools = [tool];
const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 });
const llmWithTools = llm.bindTools(tools);
const chatbot = async (state: typeof StateAnnotation.State) => {
return { messages: [await llmWithTools.invoke(state.messages)] };
};
graphBuilder.addNode("chatbot", chatbot);
const toolNode = new ToolNode(tools);
graphBuilder.addNode("tools", toolNode);
graphBuilder.addConditionalEdges(
"chatbot",
toolsCondition,
);
// Any time a tool is called, we return to the chatbot to decide the next step
graphBuilder.addEdge("tools", "chatbot");
graphBuilder.addEdge(START, "chatbot");
const graph = graphBuilder.compile();
```
:::
**Congratulations!** You've created a conversational agent in LangGraph that can use a search engine to retrieve updated information when needed. Now it can handle a wider range of user queries. To inspect all the steps your agent just took, check out this [LangSmith trace](https://smith.langchain.com/public/4fbd7636-25af-4638-9587-5a02fdbb0172/r).
## Next steps
The chatbot cannot remember past interactions on its own, which limits its ability to have coherent, multi-turn conversations. In the next part, you will [add **memory**](./3-add-memory.md) to address this.
The chatbot cannot remember past interactions on its own, which limits its ability to have coherent, multi-turn conversations. In the next part, you will [add **memory**](./3-add-memory.md) to address this.
+1 -236
View File
@@ -14,21 +14,11 @@ We will see later that **checkpointing** is _much_ more powerful than simple cha
Create a `MemorySaver` checkpointer:
:::python
``` python
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
```
:::
:::js
```typescript
import { MemorySaver } from "@langchain/langgraph";
const memory = new MemorySaver();
```
:::
This is in-memory checkpointer, which is convenient for the tutorial. However, in a production application, you would likely change this to use `SqliteSaver` or `PostgresSaver` and connect a database.
@@ -36,7 +26,6 @@ This is in-memory checkpointer, which is convenient for the tutorial. However, i
Compile the graph with the provided checkpointer, which will checkpoint the `State` as the graph works through each node:
:::python
``` python
graph = graph_builder.compile(checkpointer=memory)
```
@@ -50,27 +39,6 @@ except Exception:
# This requires some extra dependencies and is optional
pass
```
:::
:::js
```typescript
const graph = graphBuilder.compile({ checkpointer: memory });
```
```typescript
import * as tslab from "tslab";
try {
const representation = graph.getGraph();
const image = await representation.drawMermaidPng();
const arrayBuffer = await image.arrayBuffer();
await tslab.display.png(new Uint8Array(arrayBuffer));
} catch (e) {
// This requires some extra dependencies and is optional
}
```
:::
## 3. Interact with your chatbot
@@ -78,21 +46,12 @@ Now you can interact with your bot!
1. Pick a thread to use as the key for this conversation.
:::python
```python
config = {"configurable": {"thread_id": "1"}}
```
:::
:::js
```typescript
const config = { configurable: { thread_id: "1" } };
```
:::
2. Call your chatbot:
:::python
```python
user_input = "Hi there! My name is Will."
@@ -105,24 +64,6 @@ Now you can interact with your bot!
for event in events:
event["messages"][-1].pretty_print()
```
:::
:::js
```typescript
const userInput = "Hi there! My name is Will.";
// The config is the **second positional argument** to stream() or invoke()!
const events = await graph.stream(
{ messages: [{ role: "user", content: userInput }] },
{ ...config, streamMode: "values" }
);
for await (const event of events) {
const messages = event.messages;
console.log(messages[messages.length - 1]);
}
```
:::
```
================================ Human Message =================================
@@ -133,23 +74,14 @@ Now you can interact with your bot!
Hello Will! It's nice to meet you. How can I assist you today? Is there anything specific you'd like to know or discuss?
```
:::python
!!! note
The config was provided as the **second positional argument** when calling our graph. It importantly is _not_ nested within the graph inputs (`{'messages': []}`).
:::
:::js
!!! note
The config was provided as the **second positional argument** when calling our graph. It importantly is _not_ nested within the graph inputs (`{ messages: [] }`).
:::
## 4. Ask a follow up question
Ask a follow up question:
:::python
```python
user_input = "Remember my name?"
@@ -162,24 +94,6 @@ events = graph.stream(
for event in events:
event["messages"][-1].pretty_print()
```
:::
:::js
```typescript
const userInput2 = "Remember my name?";
// The config is the **second positional argument** to stream() or invoke()!
const events2 = await graph.stream(
{ messages: [{ role: "user", content: userInput2 }] },
{ ...config, streamMode: "values" }
);
for await (const event of events2) {
const messages = event.messages;
console.log(messages[messages.length - 1]);
}
```
:::
```
================================ Human Message =================================
@@ -194,7 +108,6 @@ Of course, I remember your name, Will. I always try to pay attention to importan
Don't believe me? Try this using a different config.
:::python
```python
# The only difference is we change the `thread_id` here to "2" instead of "1"
events = graph.stream(
@@ -206,23 +119,6 @@ events = graph.stream(
for event in events:
event["messages"][-1].pretty_print()
```
:::
:::js
```typescript
// The only difference is we change the `thread_id` here to "2" instead of "1"
const events3 = await graph.stream(
{ messages: [{ role: "user", content: userInput2 }] },
// highlight-next-line
{ configurable: { thread_id: "2" }, streamMode: "values" }
);
for await (const event of events3) {
const messages = event.messages;
console.log(messages[messages.length - 1]);
}
```
:::
```
================================ Human Message =================================
@@ -237,15 +133,8 @@ I apologize, but I don't have any previous context or memory of your name. As an
## 5. Inspect the state
:::python
By now, we have made a few checkpoints across two different threads. But what goes into a checkpoint? To inspect a graph's `state` for a given config at any time, call `get_state(config)`.
:::
:::js
By now, we have made a few checkpoints across two different threads. But what goes into a checkpoint? To inspect a graph's `state` for a given config at any time, call `getState(config)`.
:::
:::python
```python
snapshot = graph.get_state(config)
snapshot
@@ -258,75 +147,6 @@ StateSnapshot(values={'messages': [HumanMessage(content='Hi there! My name is Wi
```
snapshot.next # (since the graph ended this turn, `next` is empty. If you fetch a state from within a graph invocation, next tells which node will execute next)
```
:::
:::js
```typescript
const snapshot = await graph.getState(config);
console.log(snapshot);
```
```
StateSnapshot {
values: {
messages: [
HumanMessage {
content: 'Hi there! My name is Will.',
id: '8c1ca919-c553-4ebf-95d4-b59a2d61e078'
},
AIMessage {
content: "Hello Will! It's nice to meet you. How can I assist you today? Is there anything specific you'd like to know or discuss?",
id: 'run-58587b77-8c82-41e6-8a90-d62c444a261d-0'
},
HumanMessage {
content: 'Remember my name?',
id: 'daba7df6-ad75-4d6b-8057-745881cea1ca'
},
AIMessage {
content: "Of course, I remember your name, Will. I always try to pay attention to important details that users share with me. Is there anything else you'd like to talk about or any questions you have? I'm here to help with a wide range of topics or tasks.",
id: 'run-ffeaae5c-4d2d-4ddb-bd59-5d5cbf2a5af8-0'
}
]
},
next: [],
config: {
configurable: {
thread_id: '1',
checkpoint_ns: '',
checkpoint_id: '1ef7d06e-93e0-6acc-8004-f2ac846575d2'
}
},
metadata: {
source: 'loop',
writes: {
chatbot: {
messages: [
AIMessage {
content: "Of course, I remember your name, Will. I always try to pay attention to important details that users share with me. Is there anything else you'd like to talk about or any questions you have? I'm here to help with a wide range of topics or tasks.",
id: 'run-ffeaae5c-4d2d-4ddb-bd59-5d5cbf2a5af8-0'
}
]
}
},
step: 4,
parents: {}
},
createdAt: '2024-09-27T19:30:10.820758+00:00',
parentConfig: {
configurable: {
thread_id: '1',
checkpoint_ns: '',
checkpoint_id: '1ef7d06e-859f-6206-8003-e1bd3c264b8f'
}
},
tasks: []
}
```
```typescript
console.log(snapshot.next); // (since the graph ended this turn, `next` is empty. If you fetch a state from within a graph invocation, next tells which node will execute next)
```
:::
The snapshot above contains the current state values, corresponding config, and the `next` node to process. In our case, the graph has reached an `END` state, so `next` is empty.
@@ -337,25 +157,14 @@ Check out the code snippet below to review the graph from this tutorial:
{!snippets/chat_model_tabs.md!}
<!---
:::python
```python
from langchain.chat_models import init_chat_model
llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
```
:::
:::js
```typescript
import { ChatOpenAI } from "@langchain/openai";
const llm = new ChatOpenAI({ model: "gpt-4" });
```
:::
-->
:::python
```python hl_lines="36 37"
```python
from typing import Annotated
from langchain.chat_models import init_chat_model
@@ -394,50 +203,6 @@ graph_builder.set_entry_point("chatbot")
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript hl_lines="36 37"
import { Annotation } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
import { BaseMessage } from "@langchain/core/messages";
import { MemorySaver, StateGraph } from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
const StateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
});
const graphBuilder = new StateGraph(StateAnnotation);
const tool = new TavilySearchResults({ maxResults: 2 });
const tools = [tool];
const llm = new ChatOpenAI({ model: "gpt-4" });
const llmWithTools = llm.bindTools(tools);
function chatbot(state: typeof StateAnnotation.State) {
return { messages: [llmWithTools.invoke(state.messages)] };
}
graphBuilder.addNode("chatbot", chatbot);
const toolNode = new ToolNode(tools);
graphBuilder.addNode("tools", toolNode);
graphBuilder.addConditionalEdges(
"chatbot",
toolsCondition,
);
graphBuilder.addEdge("tools", "chatbot");
graphBuilder.addEdge("__start__", "chatbot");
const memory = new MemorySaver();
const graph = graphBuilder.compile({ checkpointer: memory });
```
:::
## Next steps
@@ -14,7 +14,6 @@ Starting with the existing code from the [Add memory to the chatbot](./3-add-mem
Let's first select a chat model:
:::python
{!snippets/chat_model_tabs.md!}
<!---
@@ -24,21 +23,9 @@ from langchain.chat_models import init_chat_model
llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
```
-->
:::
:::js
```typescript
import { ChatAnthropic } from "@langchain/anthropic";
const llm = new ChatAnthropic({
model: "claude-3-5-sonnet-latest",
});
```
:::
We can now incorporate it into our `StateGraph` with an additional tool:
:::python
``` python hl_lines="12 19 20 21 22 23"
from typing import Annotated
@@ -88,60 +75,6 @@ graph_builder.add_conditional_edges(
graph_builder.add_edge("tools", "chatbot")
graph_builder.add_edge(START, "chatbot")
```
:::
:::js
```typescript hl_lines="12 19 20 21 22 23"
import { tool } from "@langchain/core/tools";
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
import { z } from "zod";
import { MemorySaver } from "@langchain/langgraph";
import { StateGraph, START, END, MessagesAnnotation } from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { interrupt, Command } from "@langchain/langgraph";
const humanAssistance = tool(async ({ query }) => {
const humanResponse = interrupt({ query });
return humanResponse.data;
}, {
name: "human_assistance",
description: "Request assistance from a human.",
schema: z.object({
query: z.string().describe("Human readable question for the human")
})
});
const searchTool = new TavilySearchResults({ maxResults: 2 });
const tools = [searchTool, humanAssistance];
const llmWithTools = llm.bindTools(tools);
const chatbot = async (state: typeof MessagesAnnotation.State) => {
const message = await llmWithTools.invoke(state.messages);
// Because we will be interrupting during tool execution,
// we disable parallel tool calling to avoid repeating any
// tool invocations when we resume.
if (message.tool_calls && message.tool_calls.length > 1) {
throw new Error("Multiple tool calls not supported for this example");
}
return { messages: [message] };
};
const graphBuilder = new StateGraph(MessagesAnnotation)
.addNode("chatbot", chatbot);
const toolNode = new ToolNode(tools);
graphBuilder.addNode("tools", toolNode);
graphBuilder.addConditionalEdges(
"chatbot",
toolsCondition,
);
graphBuilder.addEdge("tools", "chatbot");
graphBuilder.addEdge(START, "chatbot");
```
:::
!!! tip
@@ -151,27 +84,16 @@ graphBuilder.addEdge(START, "chatbot");
We compile the graph with a checkpointer, as before:
:::python
```python
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript
const memory = new MemorySaver();
const graph = graphBuilder.compile({ checkpointer: memory });
```
:::
## 3. Visualize the graph (optional)
Visualizing the graph, you get the same layout as before just with the added tool!
:::python
``` python
from IPython.display import Image, display
@@ -181,19 +103,6 @@ except Exception:
# This requires some extra dependencies and is optional
pass
```
:::
:::js
```typescript
import * as tslab from "tslab";
const drawableGraph = graph.getGraph();
const image = await drawableGraph.drawMermaidPng();
const arrayBuffer = await image.arrayBuffer();
await tslab.display.png(new Uint8Array(arrayBuffer));
```
:::
![chatbot-with-tools-diagram](chatbot-with-tools.png)
@@ -201,7 +110,6 @@ await tslab.display.png(new Uint8Array(arrayBuffer));
Now, prompt the chatbot with a question that will engage the new `human_assistance` tool:
:::python
```python
user_input = "I need some expert guidance for building an AI agent. Could you request assistance for me?"
config = {"configurable": {"thread_id": "1"}}
@@ -229,49 +137,9 @@ Tool Calls:
Args:
query: A user is requesting expert guidance for building an AI agent. Could you please provide some expert advice or resources on this topic?
```
:::
:::js
```typescript
const userInput = "I need some expert guidance for building an AI agent. Could you request assistance for me?";
const config = { configurable: { thread_id: "1" }, streamMode: "values" as const };
const events = graph.stream(
{ messages: [{ role: "user", content: userInput }] },
config,
);
for await (const event of events) {
if (event.messages) {
const lastMessage = event.messages[event.messages.length - 1];
console.log(`================================ ${lastMessage.getType()} Message =================================`);
console.log(lastMessage.content);
if (lastMessage.tool_calls?.length) {
console.log("Tool Calls:");
lastMessage.tool_calls.forEach((call) => {
console.log(` ${call.name} (${call.id})`);
console.log(` Args: ${JSON.stringify(call.args)}`);
});
}
}
}
```
```
================================ Human Message =================================
I need some expert guidance for building an AI agent. Could you request assistance for me?
================================== Ai Message ==================================
I'd be happy to request expert assistance for you regarding building an AI agent. Let me use the human assistance function to get you some expert guidance.
Tool Calls:
human_assistance (toolu_01ABUqneqnuHNuo1vhfDFQCW)
Args: {"query":"A user is requesting expert guidance for building an AI agent. Could you please provide some expert advice or resources on this topic?"}
```
:::
The chatbot generated a tool call, but then execution has been interrupted. If you inspect the graph state, you see that it stopped at the tools node:
:::python
```python
snapshot = graph.get_state(config)
snapshot.next
@@ -280,20 +148,7 @@ snapshot.next
```
('tools',)
```
:::
:::js
```typescript
const snapshot = await graph.getState(config);
console.log(snapshot.next);
```
```
['tools']
```
:::
:::python
!!! info Additional information
Take a closer look at the `human_assistance` tool:
@@ -307,34 +162,11 @@ console.log(snapshot.next);
```
Similar to Python's built-in `input()` function, calling `interrupt` inside the tool will pause execution. Progress is persisted based on the [checkpointer](../../concepts/persistence.md#checkpointer-libraries); so if it is persisting with Postgres, it can resume at any time as long as the database is alive. In this example, it is persisting with the in-memory checkpointer and can resume any time if the Python kernel is running.
:::
:::js
!!! info Additional information
Take a closer look at the `human_assistance` tool:
```typescript
const humanAssistance = tool(async ({ query }) => {
const humanResponse = interrupt({ query });
return humanResponse.data;
}, {
name: "human_assistance",
description: "Request assistance from a human.",
schema: z.object({
query: z.string().describe("Human readable question for the human")
})
});
```
Similar to Python's built-in `input()` function, calling `interrupt` inside the tool will pause execution. Progress is persisted based on the [checkpointer](../../concepts/persistence.md#checkpointer-libraries); so if it is persisting with Postgres, it can resume at any time as long as the database is alive. In this example, it is persisting with the in-memory checkpointer and can resume any time if the JavaScript runtime is running.
:::
## 5. Resume execution
To resume execution, pass a [`Command`](../../concepts/low_level.md#command) object containing data expected by the tool. The format of this data can be customized based on needs. For this example, use a dict with a key `"data"`:
:::python
``` python
human_response = (
"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent."
@@ -382,47 +214,6 @@ LangGraph is likely a framework or library designed specifically for creating AI
If you'd like more specific information about LangGraph or have any questions about this recommendation, please feel free to ask, and I can request further assistance from the experts.
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
```
:::
:::js
```typescript
const humanResponse =
"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent." +
" It's much more reliable and extensible than simple autonomous agents.";
const humanCommand = new Command({ resume: { data: humanResponse } });
const resumeEvents = graph.stream(humanCommand, config);
for await (const event of resumeEvents) {
if (event.messages) {
const lastMessage = event.messages[event.messages.length - 1];
console.log(`================================ ${lastMessage.getType()} Message =================================`);
console.log(lastMessage.content);
}
}
```
```
================================== Ai Message ==================================
I'd be happy to request expert assistance for you regarding building an AI agent. Let me use the human assistance function to get you some expert guidance.
================================= Tool Message =================================
We, the experts are here to help! We'd recommend you check out LangGraph to build your agent. It's much more reliable and extensible than simple autonomous agents.
================================== Ai Message ==================================
Thank you for your patience. I've received some expert advice regarding your request for guidance on building an AI agent. Here's what the experts have suggested:
The experts recommend that you look into LangGraph for building your AI agent. They mention that LangGraph is a more reliable and extensible option compared to simple autonomous agents.
LangGraph is likely a framework or library designed specifically for creating AI agents with advanced capabilities. Here are a few points to consider based on this recommendation:
1. Reliability: The experts emphasize that LangGraph is more reliable than simpler autonomous agent approaches. This could mean it has better stability, error handling, or consistent performance.
2. Extensibility: LangGraph is described as more extensible, which suggests that it probably offers a flexible architecture that allows you to easily add new features or modify existing ones as your agent's requirements evolve.
3. Advanced capabilities: Given that it's recommended over "simple autonomous agents," LangGraph likely provides more sophisticated tools and techniques for building complex AI agents.
...
```
:::
The input has been received and processed as a tool message. Review this call's [LangSmith trace](https://smith.langchain.com/public/9f0f87e3-56a7-4dde-9c76-b71675624e91/r) to see the exact work that was done in the above call. Notice that the state is loaded in the first step so that our chatbot can continue where it left off.
@@ -430,7 +221,6 @@ The input has been received and processed as a tool message. Review this call's
Check out the code snippet below to review the graph from this tutorial:
:::python
{!snippets/chat_model_tabs.md!}
```python
@@ -481,64 +271,6 @@ graph_builder.add_edge(START, "chatbot")
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript
import { tool } from "@langchain/core/tools";
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
import { z } from "zod";
import { ChatAnthropic } from "@langchain/anthropic";
import { MemorySaver } from "@langchain/langgraph";
import { StateGraph, START, END, MessagesAnnotation } from "@langchain/langgraph";
import { ToolNode, toolsCondition } from "@langchain/langgraph/prebuilt";
import { interrupt, Command } from "@langchain/langgraph";
const llm = new ChatAnthropic({
model: "claude-3-5-sonnet-latest",
});
const humanAssistance = tool(async ({ query }) => {
const humanResponse = interrupt({ query });
return humanResponse.data;
}, {
name: "human_assistance",
description: "Request assistance from a human.",
schema: z.object({
query: z.string().describe("Human readable question for the human")
})
});
const searchTool = new TavilySearchResults({ maxResults: 2 });
const tools = [searchTool, humanAssistance];
const llmWithTools = llm.bindTools(tools);
const chatbot = async (state: typeof MessagesAnnotation.State) => {
const message = await llmWithTools.invoke(state.messages);
if (message.tool_calls && message.tool_calls.length > 1) {
throw new Error("Multiple tool calls not supported for this example");
}
return { messages: [message] };
};
const graphBuilder = new StateGraph(MessagesAnnotation)
.addNode("chatbot", chatbot);
const toolNode = new ToolNode(tools);
graphBuilder.addNode("tools", toolNode);
graphBuilder.addConditionalEdges(
"chatbot",
toolsCondition,
);
graphBuilder.addEdge("tools", "chatbot");
graphBuilder.addEdge(START, "chatbot");
const memory = new MemorySaver();
const graph = graphBuilder.compile({ checkpointer: memory });
```
:::
## Next steps
@@ -10,7 +10,6 @@ In this tutorial, you will add additional fields to the state to define complex
Update the chatbot to research the birthday of an entity by adding `name` and `birthday` keys to the state:
:::python
```python
from typing import Annotated
@@ -26,30 +25,11 @@ class State(TypedDict):
# highlight-next-line
birthday: str
```
:::
:::js
```typescript
import { Annotation } from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";
const StateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
// highlight-next-line
name: Annotation<string>,
// highlight-next-line
birthday: Annotation<string>,
});
```
:::
Adding this information to the state makes it easily accessible by other graph nodes (like a downstream node that stores or processes the information), as well as the graph's persistence layer.
## 2. Update the state inside the tool
:::python
Now, populate the state keys inside of the `human_assistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](../../concepts/low_level.md#using-inside-tools) to issue a state update from inside the tool.
``` python
@@ -95,73 +75,11 @@ def human_assistance(
# We return a Command object in the tool to update our state.
return Command(update=state_update)
```
:::
:::js
Now, populate the state keys inside of the `humanAssistance` tool. This allows a human to review the information before it is stored in the state. Use [`Command`](../../concepts/low_level.md#using-inside-tools) to issue a state update from inside the tool.
```typescript
import { tool } from "@langchain/core/tools";
import { ToolMessage } from "@langchain/core/messages";
import { z } from "zod";
import { Command, interrupt } from "@langchain/langgraph";
const humanAssistance = tool(async (input, config) => {
const { name, birthday } = input;
// Note that because we are generating a ToolMessage for a state update, we
// generally require the ID of the corresponding tool call. We can access this
// from the tool's config when it's called by a model.
const toolCallId = config?.toolCall?.id;
const humanResponse = interrupt({
question: "Is this correct?",
name: name,
birthday: birthday,
});
let verifiedName, verifiedBirthday, response;
// If the information is correct, update the state as-is.
if (humanResponse?.correct?.toLowerCase().startsWith("y")) {
verifiedName = name;
verifiedBirthday = birthday;
response = "Correct";
} else {
// Otherwise, receive information from the human reviewer.
verifiedName = humanResponse?.name || name;
verifiedBirthday = humanResponse?.birthday || birthday;
response = `Made a correction: ${JSON.stringify(humanResponse)}`;
}
// This time we explicitly update the state with a ToolMessage inside
// the tool.
const stateUpdate = {
name: verifiedName,
birthday: verifiedBirthday,
messages: [new ToolMessage({
content: response,
tool_call_id: toolCallId!
})],
};
// We return a Command object in the tool to update our state.
return new Command({ update: stateUpdate });
}, {
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
name: z.string(),
birthday: z.string(),
}),
});
```
:::
The rest of the graph stays the same.
## 3. Prompt the chatbot
:::python
Prompt the chatbot to look up the "birthday" of the LangGraph library and direct the chatbot to reach out to the `human_assistance` tool once it has the required information. By setting `name` and `birthday` in the arguments for the tool, you force the chatbot to generate proposals for these fields.
```python
@@ -180,30 +98,6 @@ for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
```
:::
:::js
Prompt the chatbot to look up the "birthday" of the LangGraph library and direct the chatbot to reach out to the `humanAssistance` tool once it has the required information. By setting `name` and `birthday` in the arguments for the tool, you force the chatbot to generate proposals for these fields.
```typescript
const userInput = "Can you look up when LangGraph was released? " +
"When you have the answer, use the humanAssistance tool for review.";
const config = { configurable: { thread_id: "1" } };
const events = graph.stream(
{ messages: [{ role: "user", content: userInput }] },
{ ...config, streamMode: "values" }
);
for await (const event of events) {
if (event.messages) {
const lastMessage = event.messages[event.messages.length - 1];
console.log(`================================ ${lastMessage._getType()} Message =================================`);
console.log(lastMessage.content);
}
}
```
:::
```
================================ Human Message =================================
@@ -236,7 +130,6 @@ We've hit the `interrupt` in the `human_assistance` tool again.
## 4. Add human assistance
:::python
The chatbot failed to identify the correct date, so supply it with information:
```python
@@ -252,32 +145,6 @@ for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
```
:::
:::js
The chatbot failed to identify the correct date, so supply it with information:
```typescript
import { Command } from "@langchain/langgraph";
const humanCommand = new Command({
resume: {
name: "LangGraph",
birthday: "Jan 17, 2024",
},
});
const resumeEvents = graph.stream(humanCommand, { ...config, streamMode: "values" });
for await (const event of resumeEvents) {
if (event.messages) {
const lastMessage = event.messages[event.messages.length - 1];
console.log(`================================ ${lastMessage._getType()} Message =================================`);
console.log(lastMessage.content);
}
}
```
:::
```
================================== Ai Message ==================================
@@ -308,25 +175,11 @@ It's worth noting that LangGraph had been in development and use for some time b
Note that these fields are now reflected in the state:
:::python
```python
snapshot = graph.get_state(config)
{k: v for k, v in snapshot.values.items() if k in ("name", "birthday")}
```
:::
:::js
```typescript
const snapshot = await graph.getState(config);
const relevantState = {
name: snapshot.values.name,
birthday: snapshot.values.birthday
};
console.log(relevantState);
```
:::
```
{'name': 'LangGraph', 'birthday': 'Jan 17, 2024'}
@@ -336,21 +189,11 @@ This makes them easily accessible to downstream nodes (e.g., a node that further
## 5. Manually update the state
:::python
LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), you can manually override a key using `graph.update_state`:
``` python
graph.update_state(config, {"name": "LangGraph (library)"})
```
:::
:::js
LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), you can manually override a key using `graph.updateState`:
```typescript
await graph.updateState(config, { name: "LangGraph (library)" });
```
:::
```
{'configurable': {'thread_id': '1',
@@ -360,7 +203,6 @@ await graph.updateState(config, { name: "LangGraph (library)" });
## 6. View the new value
:::python
If you call `graph.get_state`, you can see the new value is reflected:
``` python
@@ -368,21 +210,6 @@ snapshot = graph.get_state(config)
{k: v for k, v in snapshot.values.items() if k in ("name", "birthday")}
```
:::
:::js
If you call `graph.getState`, you can see the new value is reflected:
```typescript
const updatedSnapshot = await graph.getState(config);
const updatedState = {
name: updatedSnapshot.values.name,
birthday: updatedSnapshot.values.birthday
};
console.log(updatedState);
```
:::
```
{'name': 'LangGraph (library)', 'birthday': 'Jan 17, 2024'}
@@ -404,7 +231,6 @@ llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
```
-->
:::python
```python
from typing import Annotated
@@ -478,106 +304,8 @@ graph_builder.add_edge(START, "chatbot")
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript
import { ChatAnthropic } from "@langchain/anthropic";
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
import { tool } from "@langchain/core/tools";
import { ToolMessage, BaseMessage } from "@langchain/core/messages";
import { z } from "zod";
import { MemorySaver } from "@langchain/langgraph";
import { StateGraph, START, Annotation } from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { Command, interrupt } from "@langchain/langgraph";
const llm = new ChatAnthropic({
model: "claude-3-5-sonnet-latest",
});
const StateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
name: Annotation<string>,
birthday: Annotation<string>,
});
const humanAssistance = tool(async (input, config) => {
const { name, birthday } = input;
const toolCallId = config?.toolCall?.id;
const humanResponse = interrupt({
question: "Is this correct?",
name: name,
birthday: birthday,
});
let verifiedName, verifiedBirthday, response;
if (humanResponse?.correct?.toLowerCase().startsWith("y")) {
verifiedName = name;
verifiedBirthday = birthday;
response = "Correct";
} else {
verifiedName = humanResponse?.name || name;
verifiedBirthday = humanResponse?.birthday || birthday;
response = `Made a correction: ${JSON.stringify(humanResponse)}`;
}
const stateUpdate = {
name: verifiedName,
birthday: verifiedBirthday,
messages: [new ToolMessage({
content: response,
tool_call_id: toolCallId!
})],
};
return new Command({ update: stateUpdate });
}, {
name: "humanAssistance",
description: "Request assistance from a human.",
schema: z.object({
name: z.string(),
birthday: z.string(),
}),
});
const searchTool = new TavilySearchResults({ maxResults: 2 });
const tools = [searchTool, humanAssistance];
const llmWithTools = llm.bindTools(tools);
const chatbot = async (state: typeof StateAnnotation.State) => {
const message = await llmWithTools.invoke(state.messages);
return { messages: [message] };
};
const shouldContinue = (state: typeof StateAnnotation.State) => {
const lastMessage = state.messages[state.messages.length - 1];
if ("tool_calls" in lastMessage && lastMessage.tool_calls?.length) {
return "tools";
}
return "__end__";
};
const graphBuilder = new StateGraph(StateAnnotation);
graphBuilder.addNode("chatbot", chatbot);
const toolNode = new ToolNode(tools);
graphBuilder.addNode("tools", toolNode);
graphBuilder.addConditionalEdges("chatbot", shouldContinue);
graphBuilder.addEdge("tools", "chatbot");
graphBuilder.addEdge(START, "chatbot");
const memory = new MemorySaver();
const graph = graphBuilder.compile({ checkpointer: memory });
```
:::
## Next steps
There's one more concept to review before finishing the LangGraph basics tutorials: connecting `checkpointing` and `state updates` to [time travel](./6-time-travel.md).
There's one more concept to review before finishing the LangGraph basics tutorials: connecting `checkpointing` and `state updates` to [time travel](./6-time-travel.md).
@@ -12,35 +12,18 @@ You can create these types of experiences using LangGraph's built-in **time trav
## 1. Rewind your graph
:::python
Rewind your graph by fetching a checkpoint using the graph's `get_state_history` method. You can then resume execution at this previous point in time.
:::
:::js
Rewind your graph by fetching a checkpoint using the graph's `getStateHistory` method. You can then resume execution at this previous point in time.
:::
{!snippets/chat_model_tabs.md!}
<!---
:::python
```python
from langchain.chat_models import init_chat_model
llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
```
:::
:::js
```typescript
import { initChatModel } from "langchain/chat_models/init";
const llm = initChatModel("anthropic:claude-3-5-sonnet-latest");
```
:::
-->
:::python
```python
from typing import Annotated
@@ -80,62 +63,11 @@ graph_builder.add_edge(START, "chatbot")
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
```
:::
:::js
```typescript
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
import { ChatAnthropic } from "@langchain/anthropic";
import { BaseMessage } from "@langchain/core/messages";
import { Annotation, StateGraph, START, END } from "@langchain/langgraph";
import { MemorySaver } from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { messagesStateReducer } from "@langchain/langgraph";
const StateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: messagesStateReducer,
}),
});
const graphBuilder = new StateGraph(StateAnnotation);
const tool = new TavilySearchResults({ maxResults: 2 });
const tools = [tool];
const llm = new ChatAnthropic({ model: "claude-3-5-sonnet-latest" });
const llmWithTools = llm.bindTools(tools);
const chatbot = async (state: typeof StateAnnotation.State) => {
return { messages: [await llmWithTools.invoke(state.messages)] };
};
graphBuilder.addNode("chatbot", chatbot);
const toolNode = new ToolNode(tools);
graphBuilder.addNode("tools", toolNode);
const toolsCondition = (state: typeof StateAnnotation.State) => {
const lastMessage = state.messages[state.messages.length - 1];
if ("tool_calls" in lastMessage && lastMessage.tool_calls?.length) {
return "tools";
}
return END;
};
graphBuilder.addConditionalEdges("chatbot", toolsCondition);
graphBuilder.addEdge("tools", "chatbot");
graphBuilder.addEdge(START, "chatbot");
const memory = new MemorySaver();
const graph = graphBuilder.compile({ checkpointer: memory });
```
:::
## 2. Add steps
Add steps to your graph. Every step will be checkpointed in its state history:
:::python
``` python
config = {"configurable": {"thread_id": "1"}}
events = graph.stream(
@@ -157,42 +89,6 @@ for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
```
:::
:::js
```typescript
const config = { configurable: { thread_id: "1" } };
const events = await graph.stream(
{
messages: [
{
role: "user",
content: (
"I'm learning LangGraph. " +
"Could you do some research on it for me?"
),
},
],
},
{ ...config, streamMode: "values" }
);
for await (const event of events) {
if ("messages" in event) {
const lastMessage = event.messages[event.messages.length - 1];
console.log(`================================ ${lastMessage._getType()} Message =================================`);
console.log(lastMessage.content);
if ("tool_calls" in lastMessage && lastMessage.tool_calls?.length) {
console.log("Tool Calls:");
for (const toolCall of lastMessage.tool_calls) {
console.log(` ${toolCall.name} (${toolCall.id})`);
console.log(` Args: ${JSON.stringify(toolCall.args)}`);
}
}
}
}
```
:::
```
================================ Human Message =================================
@@ -227,7 +123,6 @@ Is there any specific aspect of LangGraph you'd like to know more about? I'd be
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
```
:::python
```python
events = graph.stream(
{
@@ -248,41 +143,6 @@ for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
```
:::
:::js
```typescript
const events2 = await graph.stream(
{
messages: [
{
role: "user",
content: (
"Ya that's helpful. Maybe I'll " +
"build an autonomous agent with it!"
),
},
],
},
{ ...config, streamMode: "values" }
);
for await (const event of events2) {
if ("messages" in event) {
const lastMessage = event.messages[event.messages.length - 1];
console.log(`================================ ${lastMessage._getType()} Message =================================`);
console.log(lastMessage.content);
if ("tool_calls" in lastMessage && lastMessage.tool_calls?.length) {
console.log("Tool Calls:");
for (const toolCall of lastMessage.tool_calls) {
console.log(` ${toolCall.name} (${toolCall.id})`);
console.log(` Args: ${JSON.stringify(toolCall.args)}`);
}
}
}
}
```
:::
```
================================ Human Message =================================
@@ -299,7 +159,7 @@ Tool Calls:
================================= Tool Message =================================
Name: tavily_search_results_json
[{"url": "https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d", "content": "Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user's question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow"}, {"url": "https://github.com/anmolaman20/Tools_and_Agents", "content": "GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph."}]
[{"url": "https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d", "content": "Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a users question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow"}, {"url": "https://github.com/anmolaman20/Tools_and_Agents", "content": "GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph."}]
================================== Ai Message ==================================
Great idea! Building an autonomous agent with LangGraph is definitely an exciting project. Based on the latest information I've found, here are some insights and tips for building autonomous agents with LangGraph:
@@ -321,7 +181,6 @@ Output is truncated. View as a scrollable element or open in a text editor. Adju
Now that you have added steps to the chatbot, you can `replay` the full state history to see everything that occurred.
:::python
``` python
to_replay = None
for state in graph.get_state_history(config):
@@ -331,24 +190,7 @@ for state in graph.get_state_history(config):
# We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state.
to_replay = state
```
:::
:::js
```typescript
let toReplay = null;
const stateHistory = await graph.getStateHistory(config);
for await (const state of stateHistory) {
console.log("Num Messages: ", state.values.messages.length, "Next: ", state.next);
console.log("-".repeat(80));
if (state.values.messages.length === 6) {
// We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state.
toReplay = state;
}
}
```
:::
:::python
```
Num Messages: 8 Next: ()
--------------------------------------------------------------------------------
@@ -371,32 +213,6 @@ Num Messages: 1 Next: ('chatbot',)
Num Messages: 0 Next: ('__start__',)
--------------------------------------------------------------------------------
```
:::
:::js
```
Num Messages: 8 Next: []
--------------------------------------------------------------------------------
Num Messages: 7 Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 6 Next: ["tools"]
--------------------------------------------------------------------------------
Num Messages: 5 Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 4 Next: ["__start__"]
--------------------------------------------------------------------------------
Num Messages: 4 Next: []
--------------------------------------------------------------------------------
Num Messages: 3 Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 2 Next: ["tools"]
--------------------------------------------------------------------------------
Num Messages: 1 Next: ["chatbot"]
--------------------------------------------------------------------------------
Num Messages: 0 Next: ["__start__"]
--------------------------------------------------------------------------------
```
:::
Checkpoints are saved for every step of the graph. This __spans invocations__ so you can rewind across a full thread's history.
@@ -404,74 +220,27 @@ Checkpoints are saved for every step of the graph. This __spans invocations__ so
Resume from the `to_replay` state, which is after the `chatbot` node in the second graph invocation. Resuming from this point will call the **action** node next.
:::python
```python
print(to_replay.next)
print(to_replay.config)
```
:::
:::js
```typescript
console.log(toReplay.next);
console.log(toReplay.config);
```
:::
:::python
```
('tools',)
{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1efd43e3-0c1f-6c4e-8006-891877d65740'}}
```
:::
:::js
```
["tools"]
{
"configurable": {
"thread_id": "1",
"checkpoint_ns": "",
"checkpoint_id": "1efd43e3-0c1f-6c4e-8006-891877d65740"
}
}
```
:::
## 4. Load a state from a moment-in-time
The checkpoint's `to_replay.config` contains a `checkpoint_id` timestamp. Providing this `checkpoint_id` value tells LangGraph's checkpointer to **load** the state from that moment in time.
:::python
``` python
# The `checkpoint_id` in the `to_replay.config` corresponds to a state we've persisted to our checkpointer.
for event in graph.stream(None, to_replay.config, stream_mode="values"):
if "messages" in event:
event["messages"][-1].pretty_print()
```
:::
:::js
```typescript
// The `checkpoint_id` in the `toReplay.config` corresponds to a state we've persisted to our checkpointer.
const timeTravel = await graph.stream(null, { ...toReplay.config, streamMode: "values" });
for await (const event of timeTravel) {
if ("messages" in event) {
const lastMessage = event.messages[event.messages.length - 1];
console.log(`================================ ${lastMessage._getType()} Message =================================`);
console.log(lastMessage.content);
if ("tool_calls" in lastMessage && lastMessage.tool_calls?.length) {
console.log("Tool Calls:");
for (const toolCall of lastMessage.tool_calls) {
console.log(` ${toolCall.name} (${toolCall.id})`);
console.log(` Args: ${JSON.stringify(toolCall.args)}`);
}
}
}
}
```
:::
```
================================== Ai Message ==================================
@@ -485,7 +254,7 @@ Tool Calls:
================================= Tool Message =================================
Name: tavily_search_results_json
[{"url": "https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d", "content": "Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user's question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow"}, {"url": "https://github.com/anmolaman20/Tools_and_Agents", "content": "GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph."}]
[{"url": "https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d", "content": "Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a users question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow"}, {"url": "https://github.com/anmolaman20/Tools_and_Agents", "content": "GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph."}]
================================== Ai Message ==================================
Great idea! Building an autonomous agent with LangGraph is indeed an excellent way to apply and deepen your understanding of the technology. Based on the search results, I can provide you with some insights and resources to help you get started:
@@ -471,7 +471,7 @@
"\n",
"_get_pass(\"TAVILY_API_KEY\")\n",
"\n",
"calculate = get_math_tool(ChatOpenAI(model=\"gpt-4o\"))\n",
"calculate = get_math_tool(ChatOpenAI(model=\"gpt-4-turbo-preview\"))\n",
"search = TavilySearchResults(\n",
" max_results=1,\n",
" description='tavily_search_results_json(query=\"the search query\") - a search engine.',\n",
@@ -540,11 +540,11 @@
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001B[1m System Message \u001B[0m================================\n",
"================================\u001b[1m System Message \u001b[0m================================\n",
"\n",
"Given a user query, create a plan to solve it with the utmost parallelizability. Each plan should comprise an action from the following \u001B[33;1m\u001B[1;3m{num_tools}\u001B[0m types:\n",
"\u001B[33;1m\u001B[1;3m{tool_descriptions}\u001B[0m\n",
"\u001B[33;1m\u001B[1;3m{num_tools}\u001B[0m. join(): Collects and combines results from prior actions.\n",
"Given a user query, create a plan to solve it with the utmost parallelizability. Each plan should comprise an action from the following \u001b[33;1m\u001b[1;3m{num_tools}\u001b[0m types:\n",
"\u001b[33;1m\u001b[1;3m{tool_descriptions}\u001b[0m\n",
"\u001b[33;1m\u001b[1;3m{num_tools}\u001b[0m. join(): Collects and combines results from prior actions.\n",
"\n",
" - An LLM agent is called upon invoking join() to either finalize the user query or wait until the plans are executed.\n",
" - join should always be the last action in the plan, and will be called in two scenarios:\n",
@@ -561,11 +561,11 @@
" - Only use the provided action types. If a query cannot be addressed using these, invoke the join action for the next steps.\n",
" - Never introduce new actions other than the ones provided.\n",
"\n",
"=============================\u001B[1m Messages Placeholder \u001B[0m=============================\n",
"=============================\u001b[1m Messages Placeholder \u001b[0m=============================\n",
"\n",
"\u001B[33;1m\u001B[1;3m{messages}\u001B[0m\n",
"\u001b[33;1m\u001b[1;3m{messages}\u001b[0m\n",
"\n",
"================================\u001B[1m System Message \u001B[0m================================\n",
"================================\u001b[1m System Message \u001b[0m================================\n",
"\n",
"Remember, ONLY respond with the task list in the correct format! E.g.:\n",
"idx. tool(arg_name=args)\n",
@@ -1030,7 +1030,7 @@
"joiner_prompt = hub.pull(\"wfh/llm-compiler-joiner\").partial(\n",
" examples=\"\"\n",
") # You can optionally add examples\n",
"llm = ChatOpenAI(model=\"gpt-4o\")\n",
"llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n",
"\n",
"runnable = joiner_prompt | llm.with_structured_output(\n",
" JoinOutputs, method=\"function_calling\"\n",
@@ -135,6 +135,7 @@
"metadata": {},
"outputs": [],
"source": [
"from langchain import hub\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"from langgraph.prebuilt import create_react_agent\n",
@@ -90,11 +90,7 @@
"id": "9ac1c2cd-81fb-40eb-8ba1-e9197800cba6",
"metadata": {},
"source": [
"## Create Index\n",
"\n",
"Set up a vector database using OpenAI Embeddings and the Chroma vector database. \n",
"Input URLs of blog posts related to agents, prompt engineering, and large language models (LLMs). \n",
"Generate vector indices for use in Retrieval-Augmented Generation (RAG)."
"## Create Index"
]
},
{
@@ -163,21 +159,6 @@
"</div>"
]
},
{
"cell_type": "markdown",
"id": "6cdd5ac0-fa18-4ee9-8051-062a0c56268f",
"metadata": {},
"source": [
"### Router for Query Analysis\n",
"\n",
"Lets start with Routing. First, assign the query analysis to the LLM.\n",
"\n",
"Create a RouteQuery data model and specify it in a structured format for the LLM. The decision for routing should be embedded in the prompt. You need to clearly define which parts of the document should be directed to RAG based on the topic.\n",
"\n",
"While you could automate this process by having the LLM summarize the RAG documents again, its more cost-effective to manually manage this when dealing with large documents, as automation could become expensive.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
@@ -238,18 +219,6 @@
"print(question_router.invoke({\"question\": \"What are the types of agent memory?\"}))"
]
},
{
"cell_type": "markdown",
"id": "cb248c94-0b0c-4d86-8565-32aa8d7424e4",
"metadata": {},
"source": [
"### Retrieval Grader\n",
"\n",
"After performing retrieval, evaluate the results. Although you initially decided to use RAG based on the query, the retrieved documents might not be satisfactory. Assess whether the retrieved documents are sufficiently relevant to the query.\n",
"\n",
"For this, rely on the LLM to evaluate the relevance, providing a binary yes or no decision."
]
},
{
"cell_type": "code",
"execution_count": 5,
@@ -340,17 +309,6 @@
"print(generation)"
]
},
{
"cell_type": "markdown",
"id": "cb0ab54a-4a4f-45fa-b1c5-cea1bf4c59d5",
"metadata": {},
"source": [
"### Hallucination Grader\n",
"\n",
"Verify if the LLM produced any hallucinations by comparing its output to the retrieved facts. \n",
"Provide the LLMs evaluation in a binary yes or no format.\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
@@ -399,16 +357,6 @@
"hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
]
},
{
"cell_type": "markdown",
"id": "4f58502a-c25f-4d80-a402-5583b0cd3e41",
"metadata": {},
"source": [
"### Answer Grader\n",
"\n",
"Evaluate the answer finally."
]
},
{
"cell_type": "code",
"execution_count": 8,
@@ -457,18 +405,6 @@
"answer_grader.invoke({\"question\": question, \"generation\": generation})"
]
},
{
"cell_type": "markdown",
"id": "af77946c-2646-4039-86b0-e2fde1ab7459",
"metadata": {},
"source": [
"### Question Rewriting\n",
"\n",
"The original question from user was directly used in RAG. \n",
"However, the users question might not be in a form suitable for RAG. \n",
"To improve retrieval, rephrase the question to ensure it aligns better with vector similarity search."
]
},
{
"cell_type": "code",
"execution_count": 9,
@@ -514,9 +450,7 @@
"id": "d07c0b31-b919-4498-869f-9673125c2473",
"metadata": {},
"source": [
"## Web Search Tool\n",
"\n",
"Use Tavily Search tool to get information from the web."
"## Web Search Tool"
]
},
{
+1 -1
View File
@@ -185,7 +185,7 @@
"\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
"\n",
"# Prompt\n",
+20 -3
View File
@@ -381,6 +381,25 @@ extra:
link: https://github.com/langchain-ai/langgraph
- icon: fontawesome/brands/twitter
link: https://twitter.com/LangChainAI
analytics:
provider: google
property: G-G8X6ELZYE0
feedback:
title: Was this page helpful?
ratings:
- icon: material/emoticon-happy-outline
name: This page was helpful
data: 1
note: >-
Thanks for your feedback!
- icon: material/emoticon-sad-outline
name: This page could be improved
data: 0
note: >-
Thanks for your feedback! Please help us improve this page by adding to the discussion below.
shared_analytics:
provider: google
property: G-47WX3HKKY2
validation:
# https://www.mkdocs.org/user-guide/configuration/
# We are still raising for omitted files because they determine the breadcrumbs for pages.
@@ -398,6 +417,4 @@ extra_css:
- stylesheets/logos.css
- stylesheets/sticky_navigation.css
- stylesheets/agent_graph_widget.css
- language-switcher.css
extra_javascript:
- language-switcher.js
-41
View File
@@ -1,41 +0,0 @@
.lang-python,
.lang-javascript {
display: none;
}
.language-switcher-global {
display: flex;
align-items: center;
padding-left: 0.5rem;
margin-right: 0.5rem;
}
/* Style the select to match the header */
.language-switcher-global select {
appearance: none;
font: inherit;
border: none;
padding: 0.25rem 0.6rem;
cursor: pointer;
outline: none;
font-weight: bolder;
}
/* Hover/focus effect */
.language-switcher-global select:hover,
.language-switcher-global select:focus {
text-decoration: underline;
}
/* Theme-specific overrides */
html[data-md-color-scheme="default"] .language-switcher-global select,
html[data-md-color-scheme="default"] .language-switcher-global option {
color: #333;
background-color: transparent;
}
html[data-md-color-scheme="slate"] .language-switcher-global select,
html[data-md-color-scheme="slate"] .language-switcher-global option {
color: #eee;
background-color: transparent;
}
-38
View File
@@ -1,38 +0,0 @@
function applyLanguageSwitching() {
const selector = document.getElementById("global-language-selector");
const langBlocks = {
python: document.querySelectorAll(".lang-python"),
javascript: document.querySelectorAll(".lang-javascript"),
};
const setLanguage = (lang) => {
for (const [key, blocks] of Object.entries(langBlocks)) {
blocks.forEach((block) => {
block.style.display = key === lang ? "block" : "none";
});
}
localStorage.setItem("preferredLang", lang);
};
const saved = localStorage.getItem("preferredLang") || "python";
if (selector) {
selector.value = saved;
selector.addEventListener("change", (e) => setLanguage(e.target.value));
}
setLanguage(saved);
}
// Run on initial load
document.addEventListener("DOMContentLoaded", applyLanguageSwitching);
// Re-run after client-side navigation (MkDocs Material)
document.addEventListener("pjax:success", applyLanguageSwitching);
// Optional: observe DOM changes (e.g., for late-loaded content)
if (window.MutationObserver) {
const observer = new MutationObserver(() => applyLanguageSwitching());
observer.observe(document.body, { childList: true, subtree: true });
}
-70
View File
@@ -1,70 +0,0 @@
{#-
This file was automatically generated - do not edit
-#}
{% set class = "md-header" %}
{% if "navigation.tabs.sticky" in features %}
{% set class = class ~ " md-header--shadow md-header--lifted" %}
{% elif "navigation.tabs" not in features %}
{% set class = class ~ " md-header--shadow" %}
{% endif %}
<header class="{{ class }}" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="{{ lang.t('header') }}">
<a href="{{ config.extra.homepage | d(nav.homepage.url, true) | url }}" title="{{ config.site_name | e }}" class="md-header__button md-logo" aria-label="{{ config.site_name }}" data-md-component="logo">
{% include "partials/logo.html" %}
</a>
<label class="md-header__button md-icon" for="__drawer">
{% set icon = config.theme.icon.menu or "material/menu" %}
{% include ".icons/" ~ icon ~ ".svg" %}
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
{{ config.site_name }}
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
{% if page.meta and page.meta.title %}
{{ page.meta.title }}
{% else %}
{{ page.title }}
{% endif %}
</span>
</div>
</div>
</div>
{% if config.theme.palette %}
{% if not config.theme.palette is mapping %}
{% include "partials/palette.html" %}
{% endif %}
{% endif %}
{% if not config.theme.palette is mapping %}
{% include "partials/javascripts/palette.html" %}
{% endif %}
{% if config.extra.alternate %}
{% include "partials/alternate.html" %}
{% endif %}
{% if "material/search" in config.plugins %}
{% set search = config.plugins["material/search"] | attr("config") %}
{% if search.enabled %}
<label class="md-header__button md-icon" for="__search">
{% set icon = config.theme.icon.search or "material/magnify" %}
{% include ".icons/" ~ icon ~ ".svg" %}
</label>
{% include "partials/search.html" %}
{% endif %}
{% endif %}
{% if config.repo_url %}
<div class="md-header__source">
{% include "partials/source.html" %}
</div>
{% endif %}
{% include "partials/language-toggle.html" %}
</nav>
{% if "navigation.tabs.sticky" in features %}
{% if "navigation.tabs" in features %}
{% include "partials/tabs.html" %}
{% endif %}
{% endif %}
</header>
@@ -1,6 +0,0 @@
<div class="md-header__button language-switcher-global" title="Select Language">
<select id="global-language-selector" aria-label="Select Language">
<option value="python">🐍 Python</option>
<option value="javascript">⚡️ JavaScript</option>
</select>
</div>
@@ -1,22 +0,0 @@
from _scripts.notebook_hooks import _apply_conditional_rendering
CONDITIONAL_RENDERING = """
above
:::js
js-content
:::
between
:::python
python-content
:::
below
"""
def test_conditional_rendering() -> None:
"""Test logic for conditional rendering of content."""
output = _apply_conditional_rendering(CONDITIONAL_RENDERING, "js")
assert output.strip() == "above\njs-content\n\nbetween\n\nbelow"
output = _apply_conditional_rendering(CONDITIONAL_RENDERING, "python")
assert output.strip() == "above\n\nbetween\npython-content\n\nbelow"
Generated
+3059 -3060
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -184,7 +184,7 @@
"\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_router = llm.with_structured_output(RouteQuery)\n",
"\n",
"# Prompt\n",
@@ -235,7 +235,7 @@
"\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
"\n",
"# Prompt\n",
@@ -328,7 +328,7 @@
"\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n",
"\n",
"# Prompt\n",
@@ -376,7 +376,7 @@
"\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeAnswer)\n",
"\n",
"# Prompt\n",
+4 -4
View File
@@ -200,11 +200,11 @@
"output_type": "stream",
"text": [
"********************Prompt[rlm/rag-prompt]********************\n",
"================================\u001B[1m Human Message \u001B[0m=================================\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",
"Question: \u001b[33;1m\u001b[1;3m{question}\u001b[0m \n",
"Context: \u001b[33;1m\u001b[1;3m{context}\u001b[0m \n",
"Answer:\n"
]
}
@@ -244,7 +244,7 @@
" binary_score: str = Field(description=\"Relevance score 'yes' or 'no'\")\n",
"\n",
" # LLM\n",
" model = ChatOpenAI(temperature=0, model=\"gpt-4o\", streaming=True)\n",
" model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n",
"\n",
" # LLM with tool and validation\n",
" llm_with_tool = model.with_structured_output(grade)\n",
+1 -1
View File
@@ -171,7 +171,7 @@
"\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
"\n",
"# Prompt\n",
+3 -3
View File
@@ -191,7 +191,7 @@
"\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
"\n",
"# Prompt\n",
@@ -284,7 +284,7 @@
"\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n",
"\n",
"# Prompt\n",
@@ -332,7 +332,7 @@
"\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeAnswer)\n",
"\n",
"# Prompt\n",
@@ -33,9 +33,7 @@
"id": "a384cc48-0425-4e8f-aafc-cfb8e56025c9",
"metadata": {},
"outputs": [],
"source": [
"%pip install -qU langchain-pinecone langchain-openai langchainhub langgraph"
]
"source": ["%pip install -qU langchain-pinecone langchain-openai langchainhub langgraph"]
},
{
"cell_type": "markdown",
@@ -53,9 +51,7 @@
"id": "ccc3dae5-1df6-48ca-af8a-50f0e6128876",
"metadata": {},
"outputs": [],
"source": [
"import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\""
]
"source": ["import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\""]
},
{
"cell_type": "code",
@@ -63,9 +59,7 @@
"id": "88637820",
"metadata": {},
"outputs": [],
"source": [
"import os\n\nos.environ[\"LANGCHAIN_PROJECT\"] = \"pinecone-devconnect\""
]
"source": ["import os\n\nos.environ[\"LANGCHAIN_PROJECT\"] = \"pinecone-devconnect\""]
},
{
"cell_type": "markdown",
@@ -83,9 +77,7 @@
"id": "565a6d44-2c9f-4fff-b1ec-eea05df9350d",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import OpenAIEmbeddings\nfrom langchain_pinecone import PineconeVectorStore\n\n# use pinecone movies database\n\n# Add to vectorDB\nvectorstore = PineconeVectorStore(\n embedding=OpenAIEmbeddings(),\n index_name=\"sample-movies\",\n text_key=\"summary\",\n)\nretriever = vectorstore.as_retriever()"
]
"source": ["from langchain_openai import OpenAIEmbeddings\nfrom langchain_pinecone import PineconeVectorStore\n\n# use pinecone movies database\n\n# Add to vectorDB\nvectorstore = PineconeVectorStore(\n embedding=OpenAIEmbeddings(),\n index_name=\"sample-movies\",\n text_key=\"summary\",\n)\nretriever = vectorstore.as_retriever()"]
},
{
"cell_type": "code",
@@ -112,9 +104,7 @@
]
}
],
"source": [
"docs = retriever.invoke(\"James Cameron\")\nfor doc in docs:\n print(\"# \" + doc.metadata[\"title\"])\n print(doc.page_content)\n print()"
]
"source": ["docs = retriever.invoke(\"James Cameron\")\nfor doc in docs:\n print(\"# \" + doc.metadata[\"title\"])\n print(doc.page_content)\n print()"]
},
{
"cell_type": "markdown",
@@ -130,32 +120,7 @@
"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"
]
"source": ["### Retrieval Grader\n\nfrom langchain import hub\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n\n# Data model\nclass 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\ngrade_prompt = hub.pull(\"efriis/self-rag-retrieval-grader\")\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments)\n\nretrieval_grader = grade_prompt | structured_llm_grader"]
},
{
"cell_type": "code",
@@ -172,9 +137,7 @@
]
}
],
"source": [
"# Test the retrieval grader\nquestion = \"movies starring jason momoa\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[0].page_content\nprint(doc_txt)\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
]
"source": ["# Test the retrieval grader\nquestion = \"movies starring jason momoa\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[0].page_content\nprint(doc_txt)\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"]
},
{
"cell_type": "markdown",
@@ -200,9 +163,7 @@
]
}
],
"source": [
"### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"
]
"source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"]
},
{
"cell_type": "code",
@@ -228,30 +189,7 @@
"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})"
]
"source": ["### Hallucination Grader\n\n\n# Data model\nclass 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\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeHallucinations)\n\n# https://smith.langchain.com/hub/efriis/self-rag-hallucination-grader\nhallucination_prompt = hub.pull(\"efriis/self-rag-hallucination-grader\")\n\nhallucination_grader = hallucination_prompt | structured_llm_grader\nprint(generation)\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"]
},
{
"cell_type": "code",
@@ -278,31 +216,7 @@
"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})"
]
"source": ["### Answer Grader\n\n\n# Data model\nclass 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\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeAnswer)\n\n# Prompt\nanswer_prompt = hub.pull(\"efriis/self-rag-answer-grader\")\n\nanswer_grader = answer_prompt | structured_llm_grader\nprint(question)\nprint(generation)\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"]
},
{
"cell_type": "code",
@@ -328,9 +242,7 @@
"output_type": "execute_result"
}
],
"source": [
"### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nre_write_prompt = hub.pull(\"efriis/self-rag-question-rewriter\")\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nprint(question)\nquestion_rewriter.invoke({\"question\": question})"
]
"source": ["### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nre_write_prompt = hub.pull(\"efriis/self-rag-question-rewriter\")\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nprint(question)\nquestion_rewriter.invoke({\"question\": question})"]
},
{
"cell_type": "markdown",
@@ -350,9 +262,7 @@
"id": "f1617e9e-66a8-4c1a-a1fe-cc936284c085",
"metadata": {},
"outputs": [],
"source": [
"from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass 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]"
]
"source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass 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",
@@ -360,9 +270,7 @@
"id": "add509d8-6682-4127-8d95-13dd37d79702",
"metadata": {},
"outputs": [],
"source": [
"### Nodes\n\n\ndef 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\ndef 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\ndef 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\ndef 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}"
]
"source": ["### Nodes\n\n\ndef 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\ndef 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\ndef 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\ndef 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",
@@ -370,9 +278,7 @@
"id": "09fc91b4",
"metadata": {},
"outputs": [],
"source": [
"### Edges\n\n\ndef 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\ndef 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\""
]
"source": ["### Edges\n\n\ndef 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\ndef 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",
@@ -425,9 +331,7 @@
]
}
],
"source": [
"from pprint import pprint\n\n# Run\ninputs = {\"question\": \"Movies that star Daniel Craig\"}\nfor 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\npprint(value[\"generation\"])"
]
"source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"Movies that star Daniel Craig\"}\nfor 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\npprint(value[\"generation\"])"]
},
{
"cell_type": "code",
@@ -435,9 +339,7 @@
"id": "4138bc51-8c84-4b8a-8d24-f7f470721f6f",
"metadata": {},
"outputs": [],
"source": [
"inputs = {\"question\": \"Which movies are about aliens?\"}\nfor 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\npprint(value[\"generation\"])"
]
"source": ["inputs = {\"question\": \"Which movies are about aliens?\"}\nfor 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\npprint(value[\"generation\"])"]
},
{
"cell_type": "code",
@@ -445,9 +347,7 @@
"id": "42369ab8-322d-434a-b5dd-2266e4cb2903",
"metadata": {},
"outputs": [],
"source": [
""
]
"source": [""]
}
],
"metadata": {
-14
View File
@@ -13,20 +13,6 @@ By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) withou
> [!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
@@ -175,7 +175,32 @@ class PostgresSaver(BasePostgresSaver):
value["channel_values"],
)
for value in values:
yield self._load_checkpoint_tuple(value)
yield CheckpointTuple(
{
"configurable": {
"thread_id": value["thread_id"],
"checkpoint_ns": value["checkpoint_ns"],
"checkpoint_id": value["checkpoint_id"],
}
},
{
**value["checkpoint"],
"channel_values": 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"]),
)
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
"""Get a checkpoint tuple from the database.
@@ -246,7 +271,32 @@ class PostgresSaver(BasePostgresSaver):
value["channel_values"],
)
return self._load_checkpoint_tuple(value)
return CheckpointTuple(
{
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": value["checkpoint_id"],
}
},
{
**value["checkpoint"],
"channel_values": self._load_blobs(value["channel_values"]),
},
value["metadata"],
(
{
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": value["parent_checkpoint_id"],
}
}
if value["parent_checkpoint_id"]
else None
),
self._load_writes(value["pending_writes"]),
)
def put(
self,
@@ -416,44 +466,5 @@ class PostgresSaver(BasePostgresSaver):
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": 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", "Conn"]
@@ -162,7 +162,32 @@ class AsyncPostgresSaver(BasePostgresSaver):
value["channel_values"],
)
for value in values:
yield await self._load_checkpoint_tuple(value)
yield CheckpointTuple(
{
"configurable": {
"thread_id": value["thread_id"],
"checkpoint_ns": value["checkpoint_ns"],
"checkpoint_id": value["checkpoint_id"],
}
},
{
**value["checkpoint"],
"channel_values": 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"]),
)
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
"""Get a checkpoint tuple from the database asynchronously.
@@ -213,7 +238,32 @@ class AsyncPostgresSaver(BasePostgresSaver):
value["channel_values"],
)
return await self._load_checkpoint_tuple(value)
return CheckpointTuple(
{
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": value["checkpoint_id"],
}
},
{
**value["checkpoint"],
"channel_values": self._load_blobs(value["channel_values"]),
},
value["metadata"],
(
{
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": 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"]),
)
async def aput(
self,
@@ -374,45 +424,6 @@ class AsyncPostgresSaver(BasePostgresSaver):
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": 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,
@@ -1317,12 +1317,12 @@ def _ensure_index_config(
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:
text_fields = index_config.get("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
+703 -705
View File
File diff suppressed because it is too large Load Diff
+650 -653
View File
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,6 @@ import json
import pathlib
import pickle
import re
import sys
from collections import deque
from collections.abc import Sequence
from datetime import date, datetime, time, timedelta, timezone
@@ -252,7 +251,6 @@ 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:
@@ -322,6 +320,13 @@ def _msgpack_default(obj: Any) -> str | ormsgpack.Ext:
(obj.__class__.__module__, obj.__class__.__name__, obj.hex),
),
)
elif isinstance(obj, bytearray):
return ormsgpack.Ext(
EXT_CONSTRUCTOR_SINGLE_ARG,
_msgpack_enc(
(obj.__class__.__module__, obj.__class__.__name__, bytes(obj)),
),
)
elif isinstance(obj, decimal.Decimal):
return ormsgpack.Ext(
EXT_CONSTRUCTOR_SINGLE_ARG,
@@ -460,22 +465,6 @@ def _msgpack_default(obj: Any) -> str | ormsgpack.Ext:
),
),
)
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:
@@ -557,17 +546,6 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
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:
@@ -648,19 +626,6 @@ def _msgpack_ext_hook_to_json(code: int, data: bytes) -> Any:
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
_option = (
@@ -496,7 +496,7 @@ def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
if not Y:
return []
if _check_numpy():
import numpy as np
import numpy as np # type: ignore[import-not-found]
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
+1 -3
View File
@@ -13,7 +13,7 @@ license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langchain-core>=0.2.38",
"ormsgpack>=1.10.0",
"ormsgpack>=1.8.0",
]
[project.urls]
@@ -29,8 +29,6 @@ dev = [
"pytest-watcher",
"mypy",
"dataclasses-json",
"numpy",
"pandas",
]
[tool.hatch.build.targets.wheel]
+11 -169
View File
@@ -11,9 +11,6 @@ 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
@@ -298,174 +295,19 @@ def test_serde_jsonplus_bytearray() -> None:
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:
def test_loads_cannot_find() -> 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)
dumped = (
"json",
b'{"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyPydanticccc"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}',
)
assert serde.loads_typed(dumped) is None, "Should return None if cannot find class"
@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()
dumped = (
"json",
b'{"lc": 2, "type": "constructor", "id": ["tests", "test_jsonpluss", "MyPydantic"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}',
)
@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"],
}
),
pd.DataFrame({"cat_col": pd.Categorical(["a", "b", "a", "c"])}),
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),
}
),
pd.DataFrame(
{"tz_datetime": pd.date_range("2024-01-01", periods=3, freq="D", tz="UTC")}
),
pd.DataFrame({"timedelta": pd.to_timedelta([1, 2, 3], unit="D")}),
pd.DataFrame({"period": pd.period_range("2024-01", periods=3, freq="M")}),
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"]),
pd.Series(pd.Categorical(["a", "b", "a", "c"])),
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)
assert serde.loads_typed(dumped) is None, "Should return None if cannot find module"
+45 -333
View File
@@ -3,10 +3,7 @@ revision = 1
requires-python = ">=3.9"
resolution-markers = [
"python_full_version >= '3.12.4'",
"python_full_version >= '3.12' and python_full_version < '3.12.4'",
"python_full_version == '3.11.*'",
"python_full_version == '3.10.*'",
"python_full_version < '3.10'",
"python_full_version < '3.12.4'",
]
[[package]]
@@ -221,7 +218,7 @@ name = "exceptiongroup"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions", marker = "python_full_version < '3.12.4'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 }
wheels = [
@@ -336,10 +333,6 @@ dev = [
{ name = "codespell" },
{ name = "dataclasses-json" },
{ name = "mypy" },
{ name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
{ name = "numpy", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "pandas" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-mock" },
@@ -350,7 +343,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "langchain-core", specifier = ">=0.2.38" },
{ name = "ormsgpack", specifier = ">=1.10.0" },
{ name = "ormsgpack", specifier = ">=1.8.0" },
]
[package.metadata.requires-dev]
@@ -358,8 +351,6 @@ dev = [
{ name = "codespell" },
{ name = "dataclasses-json" },
{ name = "mypy" },
{ name = "numpy" },
{ name = "pandas" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-mock" },
@@ -450,189 +441,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 },
]
[[package]]
name = "numpy"
version = "2.0.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.10'",
]
sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245 },
{ url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540 },
{ url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623 },
{ url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774 },
{ url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081 },
{ url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451 },
{ url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572 },
{ url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722 },
{ url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170 },
{ url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558 },
{ url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137 },
{ url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552 },
{ url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957 },
{ url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573 },
{ url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330 },
{ url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895 },
{ url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253 },
{ url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074 },
{ url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640 },
{ url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230 },
{ url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803 },
{ url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835 },
{ url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499 },
{ url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497 },
{ url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158 },
{ url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173 },
{ url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174 },
{ url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701 },
{ url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313 },
{ url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179 },
{ url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942 },
{ url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512 },
{ url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976 },
{ url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494 },
{ url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596 },
{ url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099 },
{ url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823 },
{ url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424 },
{ url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809 },
{ url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314 },
{ url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288 },
{ url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793 },
{ url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885 },
{ url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784 },
]
[[package]]
name = "numpy"
version = "2.2.6"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.10.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 }
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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 },
{ url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 },
{ url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 },
{ url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 },
{ url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 },
{ url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 },
]
[[package]]
name = "numpy"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.12.4'",
"python_full_version >= '3.12' and python_full_version < '3.12.4'",
"python_full_version == '3.11.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/db/8e12381333aea300890829a0a36bfa738cac95475d88982d538725143fd9/numpy-2.3.0.tar.gz", hash = "sha256:581f87f9e9e9db2cba2141400e160e9dd644ee248788d6f90636eeb8fd9260a6", size = 20382813 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/5f/df67435257d827eb3b8af66f585223dc2c3f2eb7ad0b50cb1dae2f35f494/numpy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3c9fdde0fa18afa1099d6257eb82890ea4f3102847e692193b54e00312a9ae9", size = 21199688 },
{ url = "https://files.pythonhosted.org/packages/e5/ce/aad219575055d6c9ef29c8c540c81e1c38815d3be1fe09cdbe53d48ee838/numpy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46d16f72c2192da7b83984aa5455baee640e33a9f1e61e656f29adf55e406c2b", size = 14359277 },
{ url = "https://files.pythonhosted.org/packages/29/6b/2d31da8e6d2ec99bed54c185337a87f8fbeccc1cd9804e38217e92f3f5e2/numpy-2.3.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a0be278be9307c4ab06b788f2a077f05e180aea817b3e41cebbd5aaf7bd85ed3", size = 5376069 },
{ url = "https://files.pythonhosted.org/packages/7d/2a/6c59a062397553ec7045c53d5fcdad44e4536e54972faa2ba44153bca984/numpy-2.3.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:99224862d1412d2562248d4710126355d3a8db7672170a39d6909ac47687a8a4", size = 6913057 },
{ url = "https://files.pythonhosted.org/packages/d5/5a/8df16f258d28d033e4f359e29d3aeb54663243ac7b71504e89deeb813202/numpy-2.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2393a914db64b0ead0ab80c962e42d09d5f385802006a6c87835acb1f58adb96", size = 14568083 },
{ url = "https://files.pythonhosted.org/packages/0a/92/0528a563dfc2cdccdcb208c0e241a4bb500d7cde218651ffb834e8febc50/numpy-2.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7729c8008d55e80784bd113787ce876ca117185c579c0d626f59b87d433ea779", size = 16929402 },
{ url = "https://files.pythonhosted.org/packages/e4/2f/e7a8c8d4a2212c527568d84f31587012cf5497a7271ea1f23332142f634e/numpy-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:06d4fb37a8d383b769281714897420c5cc3545c79dc427df57fc9b852ee0bf58", size = 15879193 },
{ url = "https://files.pythonhosted.org/packages/e2/c3/dada3f005953847fe35f42ac0fe746f6e1ea90b4c6775e4be605dcd7b578/numpy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c39ec392b5db5088259c68250e342612db82dc80ce044cf16496cf14cf6bc6f8", size = 18665318 },
{ url = "https://files.pythonhosted.org/packages/3b/ae/3f448517dedefc8dd64d803f9d51a8904a48df730e00a3c5fb1e75a60620/numpy-2.3.0-cp311-cp311-win32.whl", hash = "sha256:ee9d3ee70d62827bc91f3ea5eee33153212c41f639918550ac0475e3588da59f", size = 6601108 },
{ url = "https://files.pythonhosted.org/packages/8c/4a/556406d2bb2b9874c8cbc840c962683ac28f21efbc9b01177d78f0199ca1/numpy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:43c55b6a860b0eb44d42341438b03513cf3879cb3617afb749ad49307e164edd", size = 13021525 },
{ url = "https://files.pythonhosted.org/packages/ed/ee/bf54278aef30335ffa9a189f869ea09e1a195b3f4b93062164a3b02678a7/numpy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:2e6a1409eee0cb0316cb64640a49a49ca44deb1a537e6b1121dc7c458a1299a8", size = 10170327 },
{ url = "https://files.pythonhosted.org/packages/89/59/9df493df81ac6f76e9f05cdbe013cdb0c9a37b434f6e594f5bd25e278908/numpy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:389b85335838155a9076e9ad7f8fdba0827496ec2d2dc32ce69ce7898bde03ba", size = 20897025 },
{ url = "https://files.pythonhosted.org/packages/2f/86/4ff04335901d6cf3a6bb9c748b0097546ae5af35e455ae9b962ebff4ecd7/numpy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9498f60cd6bb8238d8eaf468a3d5bb031d34cd12556af53510f05fcf581c1b7e", size = 14129882 },
{ url = "https://files.pythonhosted.org/packages/71/8d/a942cd4f959de7f08a79ab0c7e6cecb7431d5403dce78959a726f0f57aa1/numpy-2.3.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:622a65d40d8eb427d8e722fd410ac3ad4958002f109230bc714fa551044ebae2", size = 5110181 },
{ url = "https://files.pythonhosted.org/packages/86/5d/45850982efc7b2c839c5626fb67fbbc520d5b0d7c1ba1ae3651f2f74c296/numpy-2.3.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b9446d9d8505aadadb686d51d838f2b6688c9e85636a0c3abaeb55ed54756459", size = 6647581 },
{ url = "https://files.pythonhosted.org/packages/1a/c0/c871d4a83f93b00373d3eebe4b01525eee8ef10b623a335ec262b58f4dc1/numpy-2.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:50080245365d75137a2bf46151e975de63146ae6d79f7e6bd5c0e85c9931d06a", size = 14262317 },
{ url = "https://files.pythonhosted.org/packages/b7/f6/bc47f5fa666d5ff4145254f9e618d56e6a4ef9b874654ca74c19113bb538/numpy-2.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c24bb4113c66936eeaa0dc1e47c74770453d34f46ee07ae4efd853a2ed1ad10a", size = 16633919 },
{ url = "https://files.pythonhosted.org/packages/f5/b4/65f48009ca0c9b76df5f404fccdea5a985a1bb2e34e97f21a17d9ad1a4ba/numpy-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d8d294287fdf685281e671886c6dcdf0291a7c19db3e5cb4178d07ccf6ecc67", size = 15567651 },
{ url = "https://files.pythonhosted.org/packages/f1/62/5367855a2018578e9334ed08252ef67cc302e53edc869666f71641cad40b/numpy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6295f81f093b7f5769d1728a6bd8bf7466de2adfa771ede944ce6711382b89dc", size = 18361723 },
{ url = "https://files.pythonhosted.org/packages/d4/75/5baed8cd867eabee8aad1e74d7197d73971d6a3d40c821f1848b8fab8b84/numpy-2.3.0-cp312-cp312-win32.whl", hash = "sha256:e6648078bdd974ef5d15cecc31b0c410e2e24178a6e10bf511e0557eed0f2570", size = 6318285 },
{ url = "https://files.pythonhosted.org/packages/bc/49/d5781eaa1a15acb3b3a3f49dc9e2ff18d92d0ce5c2976f4ab5c0a7360250/numpy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:0898c67a58cdaaf29994bc0e2c65230fd4de0ac40afaf1584ed0b02cd74c6fdd", size = 12732594 },
{ url = "https://files.pythonhosted.org/packages/c2/1c/6d343e030815c7c97a1f9fbad00211b47717c7fe446834c224bd5311e6f1/numpy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:bd8df082b6c4695753ad6193018c05aac465d634834dca47a3ae06d4bb22d9ea", size = 9891498 },
{ url = "https://files.pythonhosted.org/packages/73/fc/1d67f751fd4dbafc5780244fe699bc4084268bad44b7c5deb0492473127b/numpy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5754ab5595bfa2c2387d241296e0381c21f44a4b90a776c3c1d39eede13a746a", size = 20889633 },
{ url = "https://files.pythonhosted.org/packages/e8/95/73ffdb69e5c3f19ec4530f8924c4386e7ba097efc94b9c0aff607178ad94/numpy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d11fa02f77752d8099573d64e5fe33de3229b6632036ec08f7080f46b6649959", size = 14151683 },
{ url = "https://files.pythonhosted.org/packages/64/d5/06d4bb31bb65a1d9c419eb5676173a2f90fd8da3c59f816cc54c640ce265/numpy-2.3.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:aba48d17e87688a765ab1cd557882052f238e2f36545dfa8e29e6a91aef77afe", size = 5102683 },
{ url = "https://files.pythonhosted.org/packages/12/8b/6c2cef44f8ccdc231f6b56013dff1d71138c48124334aded36b1a1b30c5a/numpy-2.3.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4dc58865623023b63b10d52f18abaac3729346a7a46a778381e0e3af4b7f3beb", size = 6640253 },
{ url = "https://files.pythonhosted.org/packages/62/aa/fca4bf8de3396ddb59544df9b75ffe5b73096174de97a9492d426f5cd4aa/numpy-2.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:df470d376f54e052c76517393fa443758fefcdd634645bc9c1f84eafc67087f0", size = 14258658 },
{ url = "https://files.pythonhosted.org/packages/1c/12/734dce1087eed1875f2297f687e671cfe53a091b6f2f55f0c7241aad041b/numpy-2.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:87717eb24d4a8a64683b7a4e91ace04e2f5c7c77872f823f02a94feee186168f", size = 16628765 },
{ url = "https://files.pythonhosted.org/packages/48/03/ffa41ade0e825cbcd5606a5669962419528212a16082763fc051a7247d76/numpy-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fa264d56882b59dcb5ea4d6ab6f31d0c58a57b41aec605848b6eb2ef4a43e8", size = 15564335 },
{ url = "https://files.pythonhosted.org/packages/07/58/869398a11863310aee0ff85a3e13b4c12f20d032b90c4b3ee93c3b728393/numpy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e651756066a0eaf900916497e20e02fe1ae544187cb0fe88de981671ee7f6270", size = 18360608 },
{ url = "https://files.pythonhosted.org/packages/2f/8a/5756935752ad278c17e8a061eb2127c9a3edf4ba2c31779548b336f23c8d/numpy-2.3.0-cp313-cp313-win32.whl", hash = "sha256:e43c3cce3b6ae5f94696669ff2a6eafd9a6b9332008bafa4117af70f4b88be6f", size = 6310005 },
{ url = "https://files.pythonhosted.org/packages/08/60/61d60cf0dfc0bf15381eaef46366ebc0c1a787856d1db0c80b006092af84/numpy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:81ae0bf2564cf475f94be4a27ef7bcf8af0c3e28da46770fc904da9abd5279b5", size = 12729093 },
{ url = "https://files.pythonhosted.org/packages/66/31/2f2f2d2b3e3c32d5753d01437240feaa32220b73258c9eef2e42a0832866/numpy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:c8738baa52505fa6e82778580b23f945e3578412554d937093eac9205e845e6e", size = 9885689 },
{ url = "https://files.pythonhosted.org/packages/f1/89/c7828f23cc50f607ceb912774bb4cff225ccae7131c431398ad8400e2c98/numpy-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39b27d8b38942a647f048b675f134dd5a567f95bfff481f9109ec308515c51d8", size = 20986612 },
{ url = "https://files.pythonhosted.org/packages/dd/46/79ecf47da34c4c50eedec7511e53d57ffdfd31c742c00be7dc1d5ffdb917/numpy-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0eba4a1ea88f9a6f30f56fdafdeb8da3774349eacddab9581a21234b8535d3d3", size = 14298953 },
{ url = "https://files.pythonhosted.org/packages/59/44/f6caf50713d6ff4480640bccb2a534ce1d8e6e0960c8f864947439f0ee95/numpy-2.3.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0f1f11d0a1da54927436505a5a7670b154eac27f5672afc389661013dfe3d4f", size = 5225806 },
{ url = "https://files.pythonhosted.org/packages/a6/43/e1fd1aca7c97e234dd05e66de4ab7a5be54548257efcdd1bc33637e72102/numpy-2.3.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:690d0a5b60a47e1f9dcec7b77750a4854c0d690e9058b7bef3106e3ae9117808", size = 6735169 },
{ url = "https://files.pythonhosted.org/packages/84/89/f76f93b06a03177c0faa7ca94d0856c4e5c4bcaf3c5f77640c9ed0303e1c/numpy-2.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8b51ead2b258284458e570942137155978583e407babc22e3d0ed7af33ce06f8", size = 14330701 },
{ url = "https://files.pythonhosted.org/packages/aa/f5/4858c3e9ff7a7d64561b20580cf7cc5d085794bd465a19604945d6501f6c/numpy-2.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:aaf81c7b82c73bd9b45e79cfb9476cb9c29e937494bfe9092c26aece812818ad", size = 16692983 },
{ url = "https://files.pythonhosted.org/packages/08/17/0e3b4182e691a10e9483bcc62b4bb8693dbf9ea5dc9ba0b77a60435074bb/numpy-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f420033a20b4f6a2a11f585f93c843ac40686a7c3fa514060a97d9de93e5e72b", size = 15641435 },
{ url = "https://files.pythonhosted.org/packages/4e/d5/463279fda028d3c1efa74e7e8d507605ae87f33dbd0543cf4c4527c8b882/numpy-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d344ca32ab482bcf8735d8f95091ad081f97120546f3d250240868430ce52555", size = 18433798 },
{ url = "https://files.pythonhosted.org/packages/0e/1e/7a9d98c886d4c39a2b4d3a7c026bffcf8fbcaf518782132d12a301cfc47a/numpy-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:48a2e8eaf76364c32a1feaa60d6925eaf32ed7a040183b807e02674305beef61", size = 6438632 },
{ url = "https://files.pythonhosted.org/packages/fe/ab/66fc909931d5eb230107d016861824f335ae2c0533f422e654e5ff556784/numpy-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ba17f93a94e503551f154de210e4d50c5e3ee20f7e7a1b5f6ce3f22d419b93bb", size = 12868491 },
{ url = "https://files.pythonhosted.org/packages/ee/e8/2c8a1c9e34d6f6d600c83d5ce5b71646c32a13f34ca5c518cc060639841c/numpy-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f14e016d9409680959691c109be98c436c6249eaf7f118b424679793607b5944", size = 9935345 },
{ url = "https://files.pythonhosted.org/packages/6a/a2/f8c1133f90eaa1c11bbbec1dc28a42054d0ce74bc2c9838c5437ba5d4980/numpy-2.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80b46117c7359de8167cc00a2c7d823bdd505e8c7727ae0871025a86d668283b", size = 21070759 },
{ url = "https://files.pythonhosted.org/packages/6c/e0/4c05fc44ba28463096eee5ae2a12832c8d2759cc5bcec34ae33386d3ff83/numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:5814a0f43e70c061f47abd5857d120179609ddc32a613138cbb6c4e9e2dbdda5", size = 5301054 },
{ url = "https://files.pythonhosted.org/packages/8a/3b/6c06cdebe922bbc2a466fe2105f50f661238ea223972a69c7deb823821e7/numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ef6c1e88fd6b81ac6d215ed71dc8cd027e54d4bf1d2682d362449097156267a2", size = 6817520 },
{ url = "https://files.pythonhosted.org/packages/9d/a3/1e536797fd10eb3c5dbd2e376671667c9af19e241843548575267242ea02/numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33a5a12a45bb82d9997e2c0b12adae97507ad7c347546190a18ff14c28bbca12", size = 14398078 },
{ url = "https://files.pythonhosted.org/packages/7c/61/9d574b10d9368ecb1a0c923952aa593510a20df4940aa615b3a71337c8db/numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:54dfc8681c1906d239e95ab1508d0a533c4a9505e52ee2d71a5472b04437ef97", size = 16751324 },
{ url = "https://files.pythonhosted.org/packages/39/de/bcad52ce972dc26232629ca3a99721fd4b22c1d2bda84d5db6541913ef9c/numpy-2.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e017a8a251ff4d18d71f139e28bdc7c31edba7a507f72b1414ed902cbe48c74d", size = 12924237 },
]
[[package]]
name = "orjson"
version = "3.10.18"
@@ -714,50 +522,50 @@ wheels = [
[[package]]
name = "ormsgpack"
version = "1.10.0"
version = "1.9.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/92/36/44eed5ef8ce93cded76a576780bab16425ce7876f10d3e2e6265e46c21ea/ormsgpack-1.10.0.tar.gz", hash = "sha256:7f7a27efd67ef22d7182ec3b7fa7e9d147c3ad9be2a24656b23c989077e08b16", size = 58629 }
sdist = { url = "https://files.pythonhosted.org/packages/25/a7/462cf8ff5e29241868b82d3a5ec124d690eb6a6a5c6fa5bb1367b839e027/ormsgpack-1.9.1.tar.gz", hash = "sha256:3da6e63d82565e590b98178545e64f0f8506137b92bd31a2d04fd7c82baf5794", size = 56887 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/74/c2dd5daf069e3798d09d5746000f9b210de04df83834e5cb47f0ace51892/ormsgpack-1.10.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8a52c7ce7659459f3dc8dec9fd6a6c76f855a0a7e2b61f26090982ac10b95216", size = 376280 },
{ url = "https://files.pythonhosted.org/packages/78/7b/30ff4bffb709e8a242005a8c4d65714fd96308ad640d31cff1b85c0d8cc4/ormsgpack-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:060f67fe927582f4f63a1260726d019204b72f460cf20930e6c925a1d129f373", size = 204335 },
{ url = "https://files.pythonhosted.org/packages/8f/3f/c95b7d142819f801a0acdbd04280e8132e43b6e5a8920173e8eb92ea0e6a/ormsgpack-1.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7058ef6092f995561bf9f71d6c9a4da867b6cc69d2e94cb80184f579a3ceed5", size = 215373 },
{ url = "https://files.pythonhosted.org/packages/ef/1a/e30f4bcf386db2015d1686d1da6110c95110294d8ea04f86091dd5eb3361/ormsgpack-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f6f3509c1b0e51b15552d314b1d409321718122e90653122ce4b997f01453a", size = 216469 },
{ url = "https://files.pythonhosted.org/packages/96/fc/7e44aeade22b91883586f45b7278c118fd210834c069774891447f444fc9/ormsgpack-1.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c1edafd5c72b863b1f875ec31c529f09c872a5ff6fe473b9dfaf188ccc3227", size = 384590 },
{ url = "https://files.pythonhosted.org/packages/ec/78/f92c24e8446697caa83c122f10b6cf5e155eddf81ce63905c8223a260482/ormsgpack-1.10.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c780b44107a547a9e9327270f802fa4d6b0f6667c9c03c3338c0ce812259a0f7", size = 478891 },
{ url = "https://files.pythonhosted.org/packages/5a/75/87449690253c64bea2b663c7c8f2dbc9ad39d73d0b38db74bdb0f3947b16/ormsgpack-1.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:137aab0d5cdb6df702da950a80405eb2b7038509585e32b4e16289604ac7cb84", size = 390121 },
{ url = "https://files.pythonhosted.org/packages/69/cc/c83257faf3a5169ec29dd87121317a25711da9412ee8c1e82f2e1a00c0be/ormsgpack-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:3e666cb63030538fa5cd74b1e40cb55b6fdb6e2981f024997a288bf138ebad07", size = 121196 },
{ url = "https://files.pythonhosted.org/packages/30/27/7da748bc0d7d567950a378dee5a32477ed5d15462ab186918b5f25cac1ad/ormsgpack-1.10.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4bb7df307e17b36cbf7959cd642c47a7f2046ae19408c564e437f0ec323a7775", size = 376275 },
{ url = "https://files.pythonhosted.org/packages/7b/65/c082cc8c74a914dbd05af0341c761c73c3d9960b7432bbf9b8e1e20811af/ormsgpack-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8817ae439c671779e1127ee62f0ac67afdeaeeacb5f0db45703168aa74a2e4af", size = 204335 },
{ url = "https://files.pythonhosted.org/packages/46/62/17ef7e5d9766c79355b9c594cc9328c204f1677bc35da0595cc4e46449f0/ormsgpack-1.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f345f81e852035d80232e64374d3a104139d60f8f43c6c5eade35c4bac5590e", size = 215372 },
{ url = "https://files.pythonhosted.org/packages/4e/92/7c91e8115fc37e88d1a35e13200fda3054ff5d2e5adf017345e58cea4834/ormsgpack-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21de648a1c7ef692bdd287fb08f047bd5371d7462504c0a7ae1553c39fee35e3", size = 216470 },
{ url = "https://files.pythonhosted.org/packages/2c/86/ce053c52e2517b90e390792d83e926a7a523c1bce5cc63d0a7cd05ce6cf6/ormsgpack-1.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3a7d844ae9cbf2112c16086dd931b2acefce14cefd163c57db161170c2bfa22b", size = 384591 },
{ url = "https://files.pythonhosted.org/packages/07/e8/2ad59f2ab222c6029e500bc966bfd2fe5cb099f8ab6b7ebeb50ddb1a6fe5/ormsgpack-1.10.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e4d80585403d86d7f800cf3d0aafac1189b403941e84e90dd5102bb2b92bf9d5", size = 478892 },
{ url = "https://files.pythonhosted.org/packages/f4/73/f55e4b47b7b18fd8e7789680051bf830f1e39c03f1d9ed993cd0c3e97215/ormsgpack-1.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:da1de515a87e339e78a3ccf60e39f5fb740edac3e9e82d3c3d209e217a13ac08", size = 390122 },
{ url = "https://files.pythonhosted.org/packages/f7/87/073251cdb93d4c6241748568b3ad1b2a76281fb2002eed16a3a4043d61cf/ormsgpack-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:57c4601812684024132cbb32c17a7d4bb46ffc7daf2fddf5b697391c2c4f142a", size = 121197 },
{ url = "https://files.pythonhosted.org/packages/99/95/f3ab1a7638f6aa9362e87916bb96087fbbc5909db57e19f12ad127560e1e/ormsgpack-1.10.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4e159d50cd4064d7540e2bc6a0ab66eab70b0cc40c618b485324ee17037527c0", size = 376806 },
{ url = "https://files.pythonhosted.org/packages/6c/2b/42f559f13c0b0f647b09d749682851d47c1a7e48308c43612ae6833499c8/ormsgpack-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb47c85f3a866e29279d801115b554af0fefc409e2ed8aa90aabfa77efe5cc6", size = 204433 },
{ url = "https://files.pythonhosted.org/packages/45/42/1ca0cb4d8c80340a89a4af9e6d8951fb8ba0d076a899d2084eadf536f677/ormsgpack-1.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c28249574934534c9bd5dce5485c52f21bcea0ee44d13ece3def6e3d2c3798b5", size = 215547 },
{ url = "https://files.pythonhosted.org/packages/0a/38/184a570d7c44c0260bc576d1daaac35b2bfd465a50a08189518505748b9a/ormsgpack-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1957dcadbb16e6a981cd3f9caef9faf4c2df1125e2a1b702ee8236a55837ce07", size = 216746 },
{ url = "https://files.pythonhosted.org/packages/69/2f/1aaffd08f6b7fdc2a57336a80bdfb8df24e6a65ada5aa769afecfcbc6cc6/ormsgpack-1.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b29412558c740bf6bac156727aa85ac67f9952cd6f071318f29ee72e1a76044", size = 384783 },
{ url = "https://files.pythonhosted.org/packages/a9/63/3e53d6f43bb35e00c98f2b8ab2006d5138089ad254bc405614fbf0213502/ormsgpack-1.10.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6933f350c2041ec189fe739f0ba7d6117c8772f5bc81f45b97697a84d03020dd", size = 479076 },
{ url = "https://files.pythonhosted.org/packages/b8/19/fa1121b03b61402bb4d04e35d164e2320ef73dfb001b57748110319dd014/ormsgpack-1.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a86de06d368fcc2e58b79dece527dc8ca831e0e8b9cec5d6e633d2777ec93d0", size = 390447 },
{ url = "https://files.pythonhosted.org/packages/b0/0d/73143ecb94ac4a5dcba223402139240a75dee0cc6ba8a543788a5646407a/ormsgpack-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:35fa9f81e5b9a0dab42e09a73f7339ecffdb978d6dbf9deb2ecf1e9fc7808722", size = 121401 },
{ url = "https://files.pythonhosted.org/packages/61/f8/ec5f4e03268d0097545efaab2893aa63f171cf2959cb0ea678a5690e16a1/ormsgpack-1.10.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d816d45175a878993b7372bd5408e0f3ec5a40f48e2d5b9d8f1cc5d31b61f1f", size = 376806 },
{ url = "https://files.pythonhosted.org/packages/c1/19/b3c53284aad1e90d4d7ed8c881a373d218e16675b8b38e3569d5b40cc9b8/ormsgpack-1.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90345ccb058de0f35262893751c603b6376b05f02be2b6f6b7e05d9dd6d5643", size = 204433 },
{ url = "https://files.pythonhosted.org/packages/09/0b/845c258f59df974a20a536c06cace593698491defdd3d026a8a5f9b6e745/ormsgpack-1.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:144b5e88f1999433e54db9d637bae6fe21e935888be4e3ac3daecd8260bd454e", size = 215549 },
{ url = "https://files.pythonhosted.org/packages/61/56/57fce8fb34ca6c9543c026ebebf08344c64dbb7b6643d6ddd5355d37e724/ormsgpack-1.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2190b352509d012915921cca76267db136cd026ddee42f1b0d9624613cc7058c", size = 216747 },
{ url = "https://files.pythonhosted.org/packages/b8/3f/655b5f6a2475c8d209f5348cfbaaf73ce26237b92d79ef2ad439407dd0fa/ormsgpack-1.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:86fd9c1737eaba43d3bb2730add9c9e8b5fbed85282433705dd1b1e88ea7e6fb", size = 384785 },
{ url = "https://files.pythonhosted.org/packages/4b/94/687a0ad8afd17e4bce1892145d6a1111e58987ddb176810d02a1f3f18686/ormsgpack-1.10.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:33afe143a7b61ad21bb60109a86bb4e87fec70ef35db76b89c65b17e32da7935", size = 479076 },
{ url = "https://files.pythonhosted.org/packages/c8/34/68925232e81e0e062a2f0ac678f62aa3b6f7009d6a759e19324dbbaebae7/ormsgpack-1.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f23d45080846a7b90feabec0d330a9cc1863dc956728412e4f7986c80ab3a668", size = 390446 },
{ url = "https://files.pythonhosted.org/packages/12/ad/f4e1a36a6d1714afb7ffb74b3ababdcb96529cf4e7a216f9f7c8eda837b6/ormsgpack-1.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:534d18acb805c75e5fba09598bf40abe1851c853247e61dda0c01f772234da69", size = 121399 },
{ url = "https://files.pythonhosted.org/packages/75/8f/bb80469db9d5b10708cba6997463d140486ca7053a5d18f99b5739cfecf7/ormsgpack-1.10.0-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:efdb25cf6d54085f7ae557268d59fd2d956f1a09a340856e282d2960fe929f32", size = 376272 },
{ url = "https://files.pythonhosted.org/packages/08/9c/48f714ed3d5a153f25e3b490496e6ba214aee265a82be1b61e39019ea146/ormsgpack-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddfcb30d4b1be2439836249d675f297947f4fb8efcd3eeb6fd83021d773cadc4", size = 204314 },
{ url = "https://files.pythonhosted.org/packages/27/42/7f9edf6e5511120b5304c76c5d3a8b4719ff927555a6dba41b6f9d041b30/ormsgpack-1.10.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee0944b6ccfd880beb1ca29f9442a774683c366f17f4207f8b81c5e24cadb453", size = 215386 },
{ url = "https://files.pythonhosted.org/packages/40/87/41e14485857fbe4ed5a530677fe60dd6910a254825c0b1cb5b04baaa4be0/ormsgpack-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35cdff6a0d3ba04e40a751129763c3b9b57a602c02944138e4b760ec99ae80a1", size = 216466 },
{ url = "https://files.pythonhosted.org/packages/cb/68/769fa1c721d8aa6799c0ce98b1711ae57de3e6379b554ebf9a11be4c62ff/ormsgpack-1.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:599ccdabc19c618ef5de6e6f2e7f5d48c1f531a625fa6772313b8515bc710681", size = 384600 },
{ url = "https://files.pythonhosted.org/packages/4e/f9/b57fd387fe16753783a3cea0ed2471c727bbed4356d8a08e3f0340251870/ormsgpack-1.10.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:bf46f57da9364bd5eefd92365c1b78797f56c6f780581eecd60cd7b367f9b4d3", size = 478888 },
{ url = "https://files.pythonhosted.org/packages/3e/0f/464cdfa7f9ee817c2d94485880b6c3c4b9f22df9fcbf21c303bbfebcb3ed/ormsgpack-1.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b796f64fdf823dedb1e35436a4a6f889cf78b1aa42d3097c66e5adfd8c3bd72d", size = 390118 },
{ url = "https://files.pythonhosted.org/packages/ad/03/b9146dff5458def4c0a2b1e35c1c24e4d5e8083899aa0718b6eccba39317/ormsgpack-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:106253ac9dc08520951e556b3c270220fcb8b4fef0d30b71eedac4befa4de749", size = 121199 },
{ url = "https://files.pythonhosted.org/packages/7f/32/5f504c0695ff96aaaf0452bee522d79b5a3ee809f22fd77fdb0dd5756d86/ormsgpack-1.9.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f1f804fd9c0fd84213a6022c34172f82323b34afa7052a4af18797582cf56365", size = 382793 },
{ url = "https://files.pythonhosted.org/packages/1e/c6/64fe1270271b61495611f1d3068baedb57d76e0f93ce7156f3763fb79b32/ormsgpack-1.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eab5cec99c46276b37071d570aab98603f3d0309b3818da3247eb64bb95e5cfc", size = 213974 },
{ url = "https://files.pythonhosted.org/packages/13/56/6666d6a9b82c7d2021fce6823ff823bc373a4e7280979c1b453317678fbc/ormsgpack-1.9.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c12c6bb30e6df6fc0213b77f0a5e143f371d618be2e8eb4d555340ce01c6900", size = 217200 },
{ url = "https://files.pythonhosted.org/packages/5f/fb/b844ed1e69d8615163525a8403d7abd3548b3fbfa0f3a973808f36145a0f/ormsgpack-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994d4bbb7ee333264a3e55e30ccee063df6635d785f21a08bf52f67821454a51", size = 223648 },
{ url = "https://files.pythonhosted.org/packages/f2/26/c40c3e300f9c61a5ed7a6921656dd0d2907a8174936e1e643677585e497c/ormsgpack-1.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a668a584cf4bb6e1a6ef5a35f3f0d0fdae80cfb7237344ad19a50cce8c79317b", size = 394197 },
{ url = "https://files.pythonhosted.org/packages/2d/4c/7a4ae187f18e7abf7ab0662b473264a60a5aa4e9bff266f541a8855df163/ormsgpack-1.9.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:aaf77699203822638014c604d100f132583844d4fd01eb639a2266970c02cfdf", size = 480550 },
{ url = "https://files.pythonhosted.org/packages/b1/33/5c465dfd5571f816835bb9e371987bf081b529c64ef28a72d18b0b59902d/ormsgpack-1.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:003d7e1992b447898caf25a820b3037ec68a57864b3e2f34b64693b7d60a9984", size = 396955 },
{ url = "https://files.pythonhosted.org/packages/8f/fd/8f64f477b5c6d66e9c6343d7d3f32d7063ba20ab151dd36884e6504899ab/ormsgpack-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:67fefc77e4ba9469f79426769eb4c78acf21f22bef3ab1239a72dd728036ffc2", size = 125102 },
{ url = "https://files.pythonhosted.org/packages/d8/3b/388e7915a28db6ab3daedfd4937bd7b063c50dd1543068daa31c0a3b70ed/ormsgpack-1.9.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:16eaf32c33ab4249e242181d59e2509b8e0330d6f65c1d8bf08c3dea38fd7c02", size = 382794 },
{ url = "https://files.pythonhosted.org/packages/0f/b4/3f4afba058822bf69b274e0defe507056be0340e65363c3ebcd312b01b84/ormsgpack-1.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c70f2e5b2f9975536e8f7936a9721601dc54febe363d2d82f74c9b31d4fe1c65", size = 213974 },
{ url = "https://files.pythonhosted.org/packages/bf/be/f0e21366d51b6e28fc3a55425be6a125545370d3479bf25be081e83ee236/ormsgpack-1.9.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:17c9e18b07d69e3db2e0f8af4731040175e11bdfde78ad8e28126e9e66ec5167", size = 217200 },
{ url = "https://files.pythonhosted.org/packages/cc/90/67a23c1c880a6e5552acb45f9555b642528f89c8bcf75283a2ea64ef7175/ormsgpack-1.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73538d749096bb6470328601a2be8f7bdec28849ec6fd19595c232a5848d7124", size = 223649 },
{ url = "https://files.pythonhosted.org/packages/80/ad/116c1f970b5b4453e4faa52645517a2e5eaf1ab385ba09a5c54253d07d0e/ormsgpack-1.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:827ff71de228cfd6d07b9d6b47911aa61b1e8dc995dec3caf8fdcdf4f874bcd0", size = 394200 },
{ url = "https://files.pythonhosted.org/packages/c5/a2/b224a5ef193628a15205e473179276b87e8290d321693e4934a05cbd6ccf/ormsgpack-1.9.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7307f808b3df282c8e8ed92c6ebceeb3eea3d8eeec808438f3f212226b25e217", size = 480551 },
{ url = "https://files.pythonhosted.org/packages/b5/f4/a0f528196af6ab46e6c3f3051cf7403016bdc7b7d3e673ea5b04b145be98/ormsgpack-1.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f30aad7fb083bed1c540a3c163c6a9f63a94e3c538860bf8f13386c29b560ad5", size = 396959 },
{ url = "https://files.pythonhosted.org/packages/bc/6b/60c6f4787e3e93f5eb34fccb163753a8771465983a579e3405152f2422fd/ormsgpack-1.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:829a1b4c5bc3c38ece0c55cf91ebc09c3b987fceb24d3f680c2bcd03fd3789a4", size = 125100 },
{ url = "https://files.pythonhosted.org/packages/dd/f1/155a598cc8030526ccaaf91ba4d61530f87900645559487edba58b0a90a2/ormsgpack-1.9.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:1ede445fc3fdba219bb0e0d1f289df26a9c7602016b7daac6fafe8fe4e91548f", size = 383225 },
{ url = "https://files.pythonhosted.org/packages/23/1c/ef3097ba550fad55c79525f461febdd4e0d9cc18d065248044536f09488e/ormsgpack-1.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db50b9f918e25b289114312ed775794d0978b469831b992bdc65bfe20b91fe30", size = 214056 },
{ url = "https://files.pythonhosted.org/packages/27/77/64d0da25896b2cbb99505ca518c109d7dd1964d7fde14c10943731738b60/ormsgpack-1.9.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c7d8fc58e4333308f58ec720b1ee6b12b2b3fe2d2d8f0766ab751cb351e8757", size = 217339 },
{ url = "https://files.pythonhosted.org/packages/6c/10/c3a7fd0a0068b0bb52cccbfeb5656db895d69e895a3abbc210c4b3f98ff8/ormsgpack-1.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeee6d08c040db265cb8563444aba343ecb32cbdbe2414a489dcead9f70c6765", size = 223816 },
{ url = "https://files.pythonhosted.org/packages/43/e7/aee1238dba652f2116c2523d36fd1c5f9775436032be5c233108fd2a1415/ormsgpack-1.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2fbb8181c198bdc413a4e889e5200f010724eea4b6d5a9a7eee2df039ac04aca", size = 394287 },
{ url = "https://files.pythonhosted.org/packages/c7/09/1b452a92376f29d7a2da7c18fb01cf09978197a8eccbb8b204e72fd5a970/ormsgpack-1.9.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:16488f094ac0e2250cceea6caf72962614aa432ee11dd57ef45e1ad25ece3eff", size = 480709 },
{ url = "https://files.pythonhosted.org/packages/de/13/7fa9fee5a73af8a73a42bf8c2e69489605714f65f5a41454400a05e84a3b/ormsgpack-1.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:422d960bfd6ad88be20794f50ec7953d8f7a0f2df60e19d0e8feb994e2ed64ee", size = 397247 },
{ url = "https://files.pythonhosted.org/packages/a1/2d/2e87cb28110db0d3bb750edd4d8719b5068852a2eef5e96b0bf376bb8a81/ormsgpack-1.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:e6e2f9eab527cf43fb4a4293e493370276b1c8716cf305689202d646c6a782ef", size = 125368 },
{ url = "https://files.pythonhosted.org/packages/b8/54/0390d5d092831e4df29dbafe32402891fc14b3e6ffe5a644b16cbbc9d9bc/ormsgpack-1.9.1-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ac61c18d9dd085e8519b949f7e655f7fb07909fd09c53b4338dd33309012e289", size = 383226 },
{ url = "https://files.pythonhosted.org/packages/47/64/8b15d262d1caefead8fb22ec144f5ff7d9505fc31c22bc34598053d46fbe/ormsgpack-1.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134840b8c6615da2c24ce77bd12a46098015c808197a9995c7a2d991e1904eec", size = 214057 },
{ url = "https://files.pythonhosted.org/packages/57/00/65823609266bad4d5ed29ea753d24a3bdb01c7edaf923da80967fc31f9c5/ormsgpack-1.9.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38fd42618f626394b2c7713c5d4bcbc917254e9753d5d4cde460658b51b11a74", size = 217340 },
{ url = "https://files.pythonhosted.org/packages/a0/51/e535c50f7f87b49110233647f55300d7975139ef5e51f1adb4c55f58c124/ormsgpack-1.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d36397333ad07b9eba4c2e271fa78951bd81afc059c85a6e9f6c0eb2de07cda", size = 223815 },
{ url = "https://files.pythonhosted.org/packages/0c/ee/393e4a6de2a62124bf589602648f295a9fb3907a0e2fe80061b88899d072/ormsgpack-1.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:603063089597917d04e4c1b1d53988a34f7dc2ff1a03adcfd1cf4ae966d5fba6", size = 394287 },
{ url = "https://files.pythonhosted.org/packages/c6/d8/e56d7c3cb73a0e533e3e2a21ae5838b2aa36a9dac1ca9c861af6bae5a369/ormsgpack-1.9.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:94bbf2b185e0cb721ceaba20e64b7158e6caf0cecd140ca29b9f05a8d5e91e2f", size = 480707 },
{ url = "https://files.pythonhosted.org/packages/e6/e0/6a3c6a6dc98583a721c54b02f5195bde8f801aebdeda9b601fa2ab30ad39/ormsgpack-1.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38f380b1e8c96a712eb302b9349347385161a8e29046868ae2bfdfcb23e2692", size = 397246 },
{ url = "https://files.pythonhosted.org/packages/b0/60/0ee5d790f13507e1f75ac21fc82dc1ef29afe1f520bd0f249d65b2f4839b/ormsgpack-1.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:a4bc63fb30db94075611cedbbc3d261dd17cf2aa8ff75a0fd684cd45ca29cb1b", size = 125371 },
{ url = "https://files.pythonhosted.org/packages/85/02/ac4a2263c9aad0d455f240e1bdd41b443e5452257cf13bc188177b0dfd1f/ormsgpack-1.9.1-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e95909248bece8e88a310a913838f17ff5a39190aa4e61de909c3cd27f59744b", size = 382789 },
{ url = "https://files.pythonhosted.org/packages/81/6f/e50d070ae3a6aa7cb50849d0796ac6d72c0f8f01d5a42438c9567ab352e3/ormsgpack-1.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3939188810c5c641d6b207f29994142ae2b1c70534f7839bbd972d857ac2072", size = 213967 },
{ url = "https://files.pythonhosted.org/packages/55/1d/379734bca4f2d71ce11c7096d85276280cf13d1bb7243bf809b171c25cda/ormsgpack-1.9.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b6476344a585aea00a2acc9fd07355bf2daac04062cfdd480fa83ec3e2403b", size = 217183 },
{ url = "https://files.pythonhosted.org/packages/9a/f6/036a44ada8659b1729db5f20ba50dc1945a84a50cd4fa6b3a74d0f16fab9/ormsgpack-1.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7d8b9d53da82b31662ce5a3834b65479cf794a34befb9fc50baa51518383250", size = 223647 },
{ url = "https://files.pythonhosted.org/packages/b9/61/5c8671ab3b7cac21076169972bad9f4faa1fdda1f70e0ae78b365894b164/ormsgpack-1.9.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3933d4b0c0d404ee234dbc372836d6f2d2f4b6330c2a2fb9709ba4eaebfae7ba", size = 394232 },
{ url = "https://files.pythonhosted.org/packages/7b/70/6e9ba8c8c405dee5dffd86edf1188ef1a116421598e18df89dac0c499aae/ormsgpack-1.9.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:f824e94a7969f0aee9a6847ec232cf731a03b8734951c2a774dd4762308ea2d2", size = 480582 },
{ url = "https://files.pythonhosted.org/packages/a6/e1/2113022dd9236cea13f0ba850a5f8a640c0c9e3b07bfbbaf01409190068b/ormsgpack-1.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c1f3f2295374020f9650e4aa7af6403ff016a0d92778b4a48bb3901fd801232d", size = 396952 },
{ url = "https://files.pythonhosted.org/packages/01/d4/58ca5de3124ac975dae1a96a475c3cb9ed70c70dfba39fd4ceca53838ee6/ormsgpack-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:92eb1b4f7b168da47f547329b4b58d16d8f19508a97ce5266567385d42d81968", size = 125107 },
]
[[package]]
@@ -769,63 +577,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
]
[[package]]
name = "pandas"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
{ name = "numpy", version = "2.3.0", 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/72/51/48f713c4c728d7c55ef7444ba5ea027c26998d96d1a40953b346438602fc/pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133", size = 4484490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/2d/df6b98c736ba51b8eaa71229e8fcd91233a831ec00ab520e1e23090cc072/pandas-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:625466edd01d43b75b1883a64d859168e4556261a5035b32f9d743b67ef44634", size = 11527531 },
{ url = "https://files.pythonhosted.org/packages/77/1c/3f8c331d223f86ba1d0ed7d3ed7fcf1501c6f250882489cc820d2567ddbf/pandas-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6872d695c896f00df46b71648eea332279ef4077a409e2fe94220208b6bb675", size = 10774764 },
{ url = "https://files.pythonhosted.org/packages/1b/45/d2599400fad7fe06b849bd40b52c65684bc88fbe5f0a474d0513d057a377/pandas-2.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4dd97c19bd06bc557ad787a15b6489d2614ddaab5d104a0310eb314c724b2d2", size = 11711963 },
{ url = "https://files.pythonhosted.org/packages/66/f8/5508bc45e994e698dbc93607ee6b9b6eb67df978dc10ee2b09df80103d9e/pandas-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:034abd6f3db8b9880aaee98f4f5d4dbec7c4829938463ec046517220b2f8574e", size = 12349446 },
{ url = "https://files.pythonhosted.org/packages/f7/fc/17851e1b1ea0c8456ba90a2f514c35134dd56d981cf30ccdc501a0adeac4/pandas-2.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23c2b2dc5213810208ca0b80b8666670eb4660bbfd9d45f58592cc4ddcfd62e1", size = 12920002 },
{ url = "https://files.pythonhosted.org/packages/a1/9b/8743be105989c81fa33f8e2a4e9822ac0ad4aaf812c00fee6bb09fc814f9/pandas-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:39ff73ec07be5e90330cc6ff5705c651ace83374189dcdcb46e6ff54b4a72cd6", size = 13651218 },
{ url = "https://files.pythonhosted.org/packages/26/fa/8eeb2353f6d40974a6a9fd4081ad1700e2386cf4264a8f28542fd10b3e38/pandas-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:40cecc4ea5abd2921682b57532baea5588cc5f80f0231c624056b146887274d2", size = 11082485 },
{ url = "https://files.pythonhosted.org/packages/96/1e/ba313812a699fe37bf62e6194265a4621be11833f5fce46d9eae22acb5d7/pandas-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8adff9f138fc614347ff33812046787f7d43b3cef7c0f0171b3340cae333f6ca", size = 11551836 },
{ url = "https://files.pythonhosted.org/packages/1b/cc/0af9c07f8d714ea563b12383a7e5bde9479cf32413ee2f346a9c5a801f22/pandas-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e5f08eb9a445d07720776df6e641975665c9ea12c9d8a331e0f6890f2dcd76ef", size = 10807977 },
{ url = "https://files.pythonhosted.org/packages/ee/3e/8c0fb7e2cf4a55198466ced1ca6a9054ae3b7e7630df7757031df10001fd/pandas-2.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa35c266c8cd1a67d75971a1912b185b492d257092bdd2709bbdebe574ed228d", size = 11788230 },
{ url = "https://files.pythonhosted.org/packages/14/22/b493ec614582307faf3f94989be0f7f0a71932ed6f56c9a80c0bb4a3b51e/pandas-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a0cc77b0f089d2d2ffe3007db58f170dae9b9f54e569b299db871a3ab5bf46", size = 12370423 },
{ url = "https://files.pythonhosted.org/packages/9f/74/b012addb34cda5ce855218a37b258c4e056a0b9b334d116e518d72638737/pandas-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c06f6f144ad0a1bf84699aeea7eff6068ca5c63ceb404798198af7eb86082e33", size = 12990594 },
{ url = "https://files.pythonhosted.org/packages/95/81/b310e60d033ab64b08e66c635b94076488f0b6ce6a674379dd5b224fc51c/pandas-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ed16339bc354a73e0a609df36d256672c7d296f3f767ac07257801aa064ff73c", size = 13745952 },
{ url = "https://files.pythonhosted.org/packages/25/ac/f6ee5250a8881b55bd3aecde9b8cfddea2f2b43e3588bca68a4e9aaf46c8/pandas-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:fa07e138b3f6c04addfeaf56cc7fdb96c3b68a3fe5e5401251f231fce40a0d7a", size = 11094534 },
{ url = "https://files.pythonhosted.org/packages/94/46/24192607058dd607dbfacdd060a2370f6afb19c2ccb617406469b9aeb8e7/pandas-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf", size = 11573865 },
{ url = "https://files.pythonhosted.org/packages/9f/cc/ae8ea3b800757a70c9fdccc68b67dc0280a6e814efcf74e4211fd5dea1ca/pandas-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9d8c3187be7479ea5c3d30c32a5d73d62a621166675063b2edd21bc47614027", size = 10702154 },
{ url = "https://files.pythonhosted.org/packages/d8/ba/a7883d7aab3d24c6540a2768f679e7414582cc389876d469b40ec749d78b/pandas-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff730713d4c4f2f1c860e36c005c7cefc1c7c80c21c0688fd605aa43c9fcf09", size = 11262180 },
{ url = "https://files.pythonhosted.org/packages/01/a5/931fc3ad333d9d87b10107d948d757d67ebcfc33b1988d5faccc39c6845c/pandas-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d", size = 11991493 },
{ url = "https://files.pythonhosted.org/packages/d7/bf/0213986830a92d44d55153c1d69b509431a972eb73f204242988c4e66e86/pandas-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:404d681c698e3c8a40a61d0cd9412cc7364ab9a9cc6e144ae2992e11a2e77a20", size = 12470733 },
{ url = "https://files.pythonhosted.org/packages/a4/0e/21eb48a3a34a7d4bac982afc2c4eb5ab09f2d988bdf29d92ba9ae8e90a79/pandas-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b", size = 13212406 },
{ url = "https://files.pythonhosted.org/packages/1f/d9/74017c4eec7a28892d8d6e31ae9de3baef71f5a5286e74e6b7aad7f8c837/pandas-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be", size = 10976199 },
{ url = "https://files.pythonhosted.org/packages/d3/57/5cb75a56a4842bbd0511c3d1c79186d8315b82dac802118322b2de1194fe/pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983", size = 11518913 },
{ url = "https://files.pythonhosted.org/packages/05/01/0c8785610e465e4948a01a059562176e4c8088aa257e2e074db868f86d4e/pandas-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd", size = 10655249 },
{ url = "https://files.pythonhosted.org/packages/e8/6a/47fd7517cd8abe72a58706aab2b99e9438360d36dcdb052cf917b7bf3bdc/pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f", size = 11328359 },
{ url = "https://files.pythonhosted.org/packages/2a/b3/463bfe819ed60fb7e7ddffb4ae2ee04b887b3444feee6c19437b8f834837/pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3", size = 12024789 },
{ url = "https://files.pythonhosted.org/packages/04/0c/e0704ccdb0ac40aeb3434d1c641c43d05f75c92e67525df39575ace35468/pandas-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8", size = 12480734 },
{ url = "https://files.pythonhosted.org/packages/e9/df/815d6583967001153bb27f5cf075653d69d51ad887ebbf4cfe1173a1ac58/pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9", size = 13223381 },
{ url = "https://files.pythonhosted.org/packages/79/88/ca5973ed07b7f484c493e941dbff990861ca55291ff7ac67c815ce347395/pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390", size = 10970135 },
{ url = "https://files.pythonhosted.org/packages/24/fb/0994c14d1f7909ce83f0b1fb27958135513c4f3f2528bde216180aa73bfc/pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575", size = 12141356 },
{ url = "https://files.pythonhosted.org/packages/9d/a2/9b903e5962134497ac4f8a96f862ee3081cb2506f69f8e4778ce3d9c9d82/pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042", size = 11474674 },
{ url = "https://files.pythonhosted.org/packages/81/3a/3806d041bce032f8de44380f866059437fb79e36d6b22c82c187e65f765b/pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c", size = 11439876 },
{ url = "https://files.pythonhosted.org/packages/15/aa/3fc3181d12b95da71f5c2537c3e3b3af6ab3a8c392ab41ebb766e0929bc6/pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67", size = 11966182 },
{ url = "https://files.pythonhosted.org/packages/37/e7/e12f2d9b0a2c4a2cc86e2aabff7ccfd24f03e597d770abfa2acd313ee46b/pandas-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f", size = 12547686 },
{ url = "https://files.pythonhosted.org/packages/39/c2/646d2e93e0af70f4e5359d870a63584dacbc324b54d73e6b3267920ff117/pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249", size = 13231847 },
{ url = "https://files.pythonhosted.org/packages/38/86/d786690bd1d666d3369355a173b32a4ab7a83053cbb2d6a24ceeedb31262/pandas-2.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9efc0acbbffb5236fbdf0409c04edce96bec4bdaa649d49985427bd1ec73e085", size = 11552206 },
{ url = "https://files.pythonhosted.org/packages/9c/2f/99f581c1c5b013fcfcbf00a48f5464fb0105da99ea5839af955e045ae3ab/pandas-2.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75651c14fde635e680496148a8526b328e09fe0572d9ae9b638648c46a544ba3", size = 10796831 },
{ url = "https://files.pythonhosted.org/packages/5c/be/3ee7f424367e0f9e2daee93a3145a18b703fbf733ba56e1cf914af4b40d1/pandas-2.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5be867a0541a9fb47a4be0c5790a4bccd5b77b92f0a59eeec9375fafc2aa14", size = 11736943 },
{ url = "https://files.pythonhosted.org/packages/83/95/81c7bb8f1aefecd948f80464177a7d9a1c5e205c5a1e279984fdacbac9de/pandas-2.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84141f722d45d0c2a89544dd29d35b3abfc13d2250ed7e68394eda7564bd6324", size = 12366679 },
{ url = "https://files.pythonhosted.org/packages/d5/7a/54cf52fb454408317136d683a736bb597864db74977efee05e63af0a7d38/pandas-2.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f95a2aef32614ed86216d3c450ab12a4e82084e8102e355707a1d96e33d51c34", size = 12924072 },
{ url = "https://files.pythonhosted.org/packages/0a/bf/25018e431257f8a42c173080f9da7c592508269def54af4a76ccd1c14420/pandas-2.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e0f51973ba93a9f97185049326d75b942b9aeb472bec616a129806facb129ebb", size = 13696374 },
{ url = "https://files.pythonhosted.org/packages/db/84/5ffd2c447c02db56326f5c19a235a747fae727e4842cc20e1ddd28f990f6/pandas-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:b198687ca9c8529662213538a9bb1e60fa0bf0f6af89292eb68fea28743fcd5a", size = 11104735 },
]
[[package]]
name = "pluggy"
version = "1.6.0"
@@ -1023,27 +774,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/3a/c44a76c6bb5e9e896d9707fb1c704a31a0136950dec9514373ced0684d56/pytest_watcher-0.4.3-py3-none-any.whl", hash = "sha256:d59b1e1396f33a65ea4949b713d6884637755d641646960056a90b267c3460f9", size = 11852 },
]
[[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 }
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 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 },
]
[[package]]
name = "pyyaml"
version = "6.0.2"
@@ -1149,15 +879,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/95/3a/2e8704d19f376c799748ff9cb041225c1d59f3e7711bc5596c8cfdc24925/ruff-0.11.10-py3-none-win_arm64.whl", hash = "sha256:ef69637b35fb8b210743926778d0e45e1bffa850a7c61e428c6b971549b5f5d1", size = 10765278 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
@@ -1249,15 +970,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
]
[[package]]
name = "tzdata"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
]
[[package]]
name = "urllib3"
version = "2.4.0"
+1 -1
View File
@@ -2,9 +2,9 @@ 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 langchain_tavily import TavilySearchResults
from langgraph.graph import END, StateGraph, add_messages
from langgraph.prebuilt import ToolNode
+1
View File
@@ -2,6 +2,7 @@
"python_version": "3.12",
"dependencies": [
"langchain_community",
"langchain_tavily; python_version < '4.0'",
"langchain_anthropic",
"langchain_openai",
"wikipedia",
+5 -5
View File
@@ -3,7 +3,6 @@ import json
from typing import Annotated, Optional
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 (
@@ -18,14 +17,15 @@ 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 langchain_tavily import TavilySearchResults
from langgraph.graph import END, StateGraph
from pydantic import BaseModel, Field
from typing_extensions import TypedDict
fast_llm = ChatOpenAI(model="gpt-4o-mini")
fast_llm = ChatOpenAI(model="gpt-3.5-turbo")
# 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")
long_context_llm = ChatOpenAI(model="gpt-4-turbo-preview")
direct_gen_outline_prompt = ChatPromptTemplate.from_messages(
@@ -144,7 +144,7 @@ gen_perspectives_prompt = ChatPromptTemplate.from_messages(
)
gen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(
model="gpt-4o-mini"
model="gpt-3.5-turbo"
).with_structured_output(Perspectives)
@@ -270,7 +270,7 @@ gen_queries_prompt = ChatPromptTemplate.from_messages(
]
)
gen_queries_chain = gen_queries_prompt | ChatOpenAI(
model="gpt-4o-mini"
model="gpt-3.5-turbo"
).with_structured_output(Queries, include_raw=True)
@@ -3,9 +3,9 @@ from pathlib import Path
from typing import Annotated, 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 langchain_tavily import TavilySearchResults
from langgraph.graph import END, StateGraph, add_messages
from langgraph.prebuilt import ToolNode
@@ -3,9 +3,9 @@ from pathlib import Path
from typing import Annotated, 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 langchain_tavily import TavilySearchResults
from langgraph.graph import END, StateGraph, add_messages
from langgraph.prebuilt import ToolNode
+1
View File
@@ -2,6 +2,7 @@
"pip_config_file": "./pipconf.txt",
"dependencies": [
"langchain_community",
"langchain_tavily; python_version < '4.0'",
"langchain_anthropic",
"langchain_openai",
"wikipedia",
+6 -59
View File
@@ -1,7 +1,6 @@
import json
import os
import pathlib
import re
import textwrap
from collections import Counter
from typing import Any, Literal, NamedTuple, Optional, TypedDict, Union
@@ -383,14 +382,6 @@ class Config(TypedDict, total=False):
Only relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.
"""
pip_installer: Optional[str]
"""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.
@@ -470,7 +461,7 @@ class Config(TypedDict, total=False):
PIP_CLEANUP_LINES = """# -- 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
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir --no-deps -e /api
# -- End of ensuring user deps didn't inadvertently overwrite langgraph-api --
# -- Removing pip from the final image ~<:===~~~ --
RUN pip uninstall -y pip setuptools wheel && \
@@ -479,7 +470,6 @@ RUN pip uninstall -y pip setuptools wheel && \
# pip removal for wolfi
RUN rm -rf /usr/lib/python*/site-packages/pip* /usr/lib/python*/site-packages/setuptools* /usr/lib/python*/site-packages/wheel* && \
find /usr/bin -name "pip*" -delete || true
{uv_removal}
# -- End of pip removal --"""
@@ -544,7 +534,6 @@ def validate_config(config: Config) -> Config:
"node_version": node_version,
"python_version": python_version,
"pip_config_file": config.get("pip_config_file"),
"pip_installer": config.get("pip_installer", "auto"),
"_INTERNAL_docker_tag": config.get("_INTERNAL_docker_tag"),
"base_image": config.get("base_image"),
"image_distro": image_distro,
@@ -609,13 +598,6 @@ def validate_config(config: Config) -> Config:
"Must be either 'debian' or 'wolfi'."
)
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:
@@ -1107,47 +1089,16 @@ def _get_node_pm_install_cmd(config_path: pathlib.Path, config: Config) -> str:
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 python_config_to_docker(
config_path: pathlib.Path,
config: Config,
base_image: str,
) -> tuple[str, dict[str, str]]:
"""Generate a Dockerfile from the configuration."""
pip_installer = config.get("pip_installer", "auto")
if pip_installer == "uv":
install_cmd = "uv pip install --system"
uv_removal = "RUN uv pip uninstall --system pip setuptools wheel && rm /usr/bin/uv /usr/bin/uvx"
elif pip_installer == "pip":
install_cmd = "pip install"
uv_removal = ""
else:
if _image_supports_uv(base_image):
install_cmd = "uv pip install --system"
uv_removal = "RUN uv pip uninstall --system pip setuptools wheel && rm /usr/bin/uv /usr/bin/uvx"
else:
install_cmd = "pip install"
uv_removal = ""
# configure pip
pip_install = f"PYTHONDONTWRITEBYTECODE=1 {install_cmd} --no-cache-dir -c /api/constraints.txt"
pip_install = (
"PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt"
)
if config.get("pip_config_file"):
pip_install = f"PIP_CONFIG_FILE=/pipconfig.txt {pip_install}"
pip_config_file_str = (
@@ -1200,10 +1151,7 @@ RUN set -ex && \\
'name = "{fullpath.name}"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]' \\
'[build-system]' \\
'requires = ["setuptools>=61"]' \\
'build-backend = "setuptools.build_meta"'; do \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_{fullpath.name}/pyproject.toml; \\
done
# -- End of non-package dependency {fullpath.name} --"""
@@ -1292,8 +1240,7 @@ ADD {relpath} /deps/{name}
"",
js_inst_str,
"",
# Add pip cleanup after all installations are complete
PIP_CLEANUP_LINES.format(install_cmd=install_cmd, uv_removal=uv_removal),
PIP_CLEANUP_LINES, # Add pip cleanup after all installations are complete
"",
f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "",
]
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-cli"
version = "0.3.2"
version = "0.2.12"
description = "CLI for interacting with LangGraph API"
authors = []
requires-python = ">=3.9"
-22
View File
@@ -134,17 +134,6 @@
],
"description": "Optional. Linux distribution for the base image.\n\nMust be either 'debian' or 'wolfi'. If omitted, defaults to 'debian'.\n"
},
"pip_installer": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional. Python package installer to use ('auto', 'pip', 'uv').\n\n"
},
"store": {
"anyOf": [
{
@@ -298,17 +287,6 @@
],
"description": "Optional. Linux distribution for the base image.\n\nMust be either 'debian' or 'wolfi'. If omitted, defaults to 'debian'.\n"
},
"pip_installer": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional. Python package installer to use ('auto', 'pip', 'uv').\n\n"
},
"store": {
"anyOf": [
{
-22
View File
@@ -134,17 +134,6 @@
],
"description": "Optional. Linux distribution for the base image.\n\nMust be either 'debian' or 'wolfi'. If omitted, defaults to 'debian'.\n"
},
"pip_installer": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional. Python package installer to use ('auto', 'pip', 'uv').\n\n"
},
"store": {
"anyOf": [
{
@@ -298,17 +287,6 @@
],
"description": "Optional. Linux distribution for the base image.\n\nMust be either 'debian' or 'wolfi'. If omitted, defaults to 'debian'.\n"
},
"pip_installer": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional. Python package installer to use ('auto', 'pip', 'uv').\n\n"
},
"store": {
"anyOf": [
{
+2 -6
View File
@@ -14,10 +14,6 @@ from langgraph_cli.config import PIP_CLEANUP_LINES, Config, validate_config
from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version
from langgraph_cli.util import clean_empty_lines
FORMATTED_CLEANUP_LINES = PIP_CLEANUP_LINES.format(
install_cmd="uv pip install --system",
uv_removal="RUN uv pip uninstall --system pip setuptools wheel && rm /usr/bin/uv /usr/bin/uvx",
)
DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities(
version_docker=Version(26, 1, 1),
version_compose=Version(2, 27, 0),
@@ -148,10 +144,10 @@ services:
COPY --from=cli_1 . /deps/cli_1
# -- End of local package ../../.. --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
WORKDIR /deps/cli
develop:
+43 -216
View File
@@ -1,4 +1,3 @@
import copy
import json
import os
import pathlib
@@ -18,11 +17,6 @@ from langgraph_cli.config import (
)
from langgraph_cli.util import clean_empty_lines
FORMATTED_CLEANUP_LINES = PIP_CLEANUP_LINES.format(
install_cmd="uv pip install --system",
uv_removal="RUN uv pip uninstall --system pip setuptools wheel && rm /usr/bin/uv /usr/bin/uvx",
)
PATH_TO_CONFIG = pathlib.Path(__file__).parent / "test_config.json"
@@ -41,7 +35,6 @@ def test_validate_config():
"python_version": "3.11",
"node_version": None,
"pip_config_file": None,
"pip_installer": "auto",
"image_distro": "debian",
"dockerfile_lines": [],
"env": {},
@@ -63,7 +56,6 @@ def test_validate_config():
"python_version": "3.12",
"node_version": None,
"pip_config_file": "pipconfig.txt",
"pip_installer": "auto",
"image_distro": "debian",
"dockerfile_lines": ["ARG meow"],
"dependencies": [".", "langchain"],
@@ -219,74 +211,6 @@ def test_validate_config_image_distro():
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)
@@ -421,7 +345,7 @@ def test_config_to_docker_simple():
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
RUN PYTHONDONTWRITEBYTECODE=1 pip install --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
@@ -433,10 +357,7 @@ RUN set -ex && \\
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]' \\
'[build-system]' \\
'requires = ["setuptools>=61"]' \\
'build-backend = "setuptools.build_meta"'; do \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
@@ -447,19 +368,16 @@ RUN set -ex && \\
'name = "graphs_reqs_a"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]' \\
'[build-system]' \\
'requires = ["setuptools>=61"]' \\
'build-backend = "setuptools.build_meta"'; do \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_graphs_reqs_a/pyproject.toml; \\
done
# -- End of non-package dependency graphs_reqs_a --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- 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}
{PIP_CLEANUP_LINES}
WORKDIR /deps/__outer_unit_tests/unit_tests\
"""
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
@@ -489,10 +407,7 @@ RUN set -ex && \\
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]' \\
'[build-system]' \\
'requires = ["setuptools>=61"]' \\
'build-backend = "setuptools.build_meta"'; do \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
@@ -503,19 +418,16 @@ RUN set -ex && \\
'name = "tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]' \\
'[build-system]' \\
'requires = ["setuptools>=61"]' \\
'build-backend = "setuptools.build_meta"'; do \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_tests/pyproject.toml; \\
done
# -- End of non-package dependency tests --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
"""
+ FORMATTED_CLEANUP_LINES
+ PIP_CLEANUP_LINES
+ """
WORKDIR /deps/__outer_unit_tests/unit_tests\
"""
@@ -550,19 +462,16 @@ RUN set -ex && \\
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]' \\
'[build-system]' \\
'requires = ["setuptools>=61"]' \\
'build-backend = "setuptools.build_meta"'; do \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Installing all local dependencies --
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
"""
+ FORMATTED_CLEANUP_LINES
+ PIP_CLEANUP_LINES
+ """
WORKDIR /deps/__outer_unit_tests/unit_tests\
"""
@@ -612,18 +521,15 @@ RUN set -ex && \\
'name = "graphs"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]' \\
'[build-system]' \\
'requires = ["setuptools>=61"]' \\
'build-backend = "setuptools.build_meta"'; do \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\
done
# -- End of non-package dependency graphs --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_graphs/src/agent.py:graph"}}'
{FORMATTED_CLEANUP_LINES}\
{PIP_CLEANUP_LINES}\
"""
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
assert additional_contexts == {}
@@ -656,11 +562,11 @@ dependencies = ["langchain"]"""
ADD . /deps/unit_tests
# -- End of local package . --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{"agent": "/deps/unit_tests/graphs/agent.py:graph"}'
"""
+ FORMATTED_CLEANUP_LINES
+ PIP_CLEANUP_LINES
+ "\n"
+ "WORKDIR /deps/unit_tests"
""
@@ -688,7 +594,7 @@ def test_config_to_docker_end_to_end():
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
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain langchain_openai
# -- Adding non-package dependency graphs --
ADD ./graphs/ /deps/__outer_graphs/src
RUN set -ex && \\
@@ -696,18 +602,15 @@ RUN set -ex && \\
'name = "graphs"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]' \\
'[build-system]' \\
'requires = ["setuptools>=61"]' \\
'build-backend = "setuptools.build_meta"'; do \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\
done
# -- End of non-package dependency graphs --
# -- Installing all local dependencies --
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_graphs/src/agent.py:graph"}}'
{FORMATTED_CLEANUP_LINES}"""
{PIP_CLEANUP_LINES}"""
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
assert additional_contexts == {}
@@ -802,15 +705,12 @@ RUN set -ex && \\
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]' \\
'[build-system]' \\
'requires = ["setuptools>=61"]' \\
'build-backend = "setuptools.build_meta"'; do \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- End of local dependencies install --
ENV LANGGRAPH_UI='{{"agent": "./graphs/agent.ui.jsx"}}'
ENV LANGGRAPH_UI_CONFIG='{{"shared": ["nuqs"]}}'
@@ -819,7 +719,7 @@ ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:g
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}
{PIP_CLEANUP_LINES}
WORKDIR /deps/__outer_unit_tests/unit_tests"""
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
@@ -848,87 +748,29 @@ RUN set -ex && \\
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]' \\
'[build-system]' \\
'requires = ["setuptools>=61"]' \\
'build-backend = "setuptools.build_meta"'; do \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- 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}
{PIP_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, "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, "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, "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, "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, "langchain/langgraph-api:0.2.47"
)
assert "uv pip install --system" in docker_default
# 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
# Create a properly indented version of PIP_CLEANUP_LINES for compose files
expected_compose_stdin = f"""
pull_policy: build
build:
@@ -942,18 +784,15 @@ def test_config_to_compose_simple_config():
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]' \\
'[build-system]' \\
'requires = ["setuptools>=61"]' \\
'build-backend = "setuptools.build_meta"'; do \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- 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), " ")}
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
WORKDIR /deps/__outer_unit_tests/unit_tests
"""
actual_compose_stdin = config_to_compose(
@@ -983,18 +822,15 @@ def test_config_to_compose_env_vars():
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]' \\
'[build-system]' \\
'requires = ["setuptools>=61"]' \\
'build-backend = "setuptools.build_meta"'; do \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- 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), " ")}
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
WORKDIR /deps/__outer_unit_tests/unit_tests
"""
openai_api_key = "key"
@@ -1028,18 +864,15 @@ def test_config_to_compose_env_file():
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]' \\
'[build-system]' \\
'requires = ["setuptools>=61"]' \\
'build-backend = "setuptools.build_meta"'; do \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- 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), " ")}
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
WORKDIR /deps/__outer_unit_tests/unit_tests
"""
actual_compose_stdin = config_to_compose(
@@ -1066,18 +899,15 @@ def test_config_to_compose_watch():
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]' \\
'[build-system]' \\
'requires = ["setuptools>=61"]' \\
'build-backend = "setuptools.build_meta"'; do \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- 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), " ")}
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
WORKDIR /deps/__outer_unit_tests/unit_tests
develop:
@@ -1113,18 +943,15 @@ def test_config_to_compose_end_to_end():
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]' \\
'[build-system]' \\
'requires = ["setuptools>=61"]' \\
'build-backend = "setuptools.build_meta"'; do \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
# -- End of non-package dependency unit_tests --
# -- Installing all local dependencies --
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
# -- 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), " ")}
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
WORKDIR /deps/__outer_unit_tests/unit_tests
develop:
+1 -1
View File
@@ -501,7 +501,7 @@ wheels = [
[[package]]
name = "langgraph-cli"
version = "0.3.2"
version = "0.2.12"
source = { editable = "." }
dependencies = [
{ name = "click" },
+1
View File
@@ -12,6 +12,7 @@
[![Downloads](https://static.pepy.tech/badge/langgraph/month)](https://pepy.tech/project/langgraph)
[![Open Issues](https://img.shields.io/github/issues-raw/langchain-ai/langgraph)](https://github.com/langchain-ai/langgraph/issues)
[![Docs](https://img.shields.io/badge/docs-latest-blue)](https://langchain-ai.github.io/langgraph/)
[![GitMCP](https://img.shields.io/endpoint?url=https://gitmcp.io/badge/langchain-ai/langgraph)](https://gitmcp.io/langchain-ai/langgraph)
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.
+13
View File
@@ -39,6 +39,8 @@ ERROR = sys.intern("__error__")
# for errors raised by nodes
NO_WRITES = sys.intern("__no_writes__")
# marker to signal node didn't write anything
SCHEDULED = sys.intern("__scheduled__")
# marker to signal node was scheduled (in distributed mode)
TASKS = sys.intern("__pregel_tasks")
# for Send objects returned by nodes/edges, corresponds to PUSH below
RETURN = sys.intern("__return__")
@@ -69,6 +71,13 @@ 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_DEDUPE_TASKS = sys.intern("__pregel_dedupe_tasks")
# holds a boolean indicating if tasks should be deduplicated (for distributed mode)
CONFIG_KEY_ENSURE_LATEST = sys.intern("__pregel_ensure_latest")
# holds a boolean indicating whether to assert the requested checkpoint is the latest
# (for distributed mode)
CONFIG_KEY_DELEGATE = sys.intern("__pregel_delegate")
# holds a boolean indicating whether to delegate subgraphs (for distributed mode)
CONFIG_KEY_THREAD_ID = sys.intern("thread_id")
# holds the thread ID for the current invocation
CONFIG_KEY_CHECKPOINT_MAP = sys.intern("checkpoint_map")
@@ -112,6 +121,7 @@ RESERVED = {
RESUME,
ERROR,
NO_WRITES,
SCHEDULED,
# reserved config.configurable keys
CONFIG_KEY_SEND,
CONFIG_KEY_READ,
@@ -122,6 +132,9 @@ RESERVED = {
CONFIG_KEY_CHECKPOINT_MAP,
CONFIG_KEY_RESUMING,
CONFIG_KEY_TASK_ID,
CONFIG_KEY_DEDUPE_TASKS,
CONFIG_KEY_ENSURE_LATEST,
CONFIG_KEY_DELEGATE,
CONFIG_KEY_CHECKPOINT_MAP,
CONFIG_KEY_CHECKPOINT_ID,
CONFIG_KEY_CHECKPOINT_NS,
+13
View File
@@ -78,6 +78,13 @@ class NodeInterrupt(GraphInterrupt):
super().__init__([Interrupt(value=value)])
class GraphDelegate(GraphBubbleUp):
"""Raised when a graph is delegated (for distributed mode)."""
def __init__(self, *args: dict[str, Any]) -> None:
super().__init__(*args)
class ParentCommand(GraphBubbleUp):
args: tuple[Command]
@@ -95,3 +102,9 @@ class TaskNotFound(Exception):
"""Raised when the executor is unable to find a task (for distributed mode)."""
pass
class CheckpointNotLatest(Exception):
"""Raised when the checkpoint is not the latest version (for distributed mode)."""
pass
+1 -1
View File
@@ -499,7 +499,7 @@ class entrypoint:
func.__name__: PregelNode(
bound=bound,
triggers=[START],
channels=START,
channels=[START],
writers=[
ChannelWrite(
[
+15 -5
View File
@@ -849,6 +849,13 @@ class StateGraph(Generic[StateT, InputT, OutputT]):
builder=self,
schema_to_mapper={},
config_type=self.config_schema,
input_model=(
self.input_schema
if len(self.channels) > 1
and isclass(self.input_schema)
and issubclass(self.input_schema, BaseModel)
else None
),
nodes={},
channels={
**self.channels,
@@ -989,17 +996,20 @@ class CompiledStateGraph(
self.nodes[key] = PregelNode(
tags=[TAG_HIDDEN],
triggers=[START],
channels=START,
channels=[START],
writers=[ChannelWrite(write_entries)],
)
elif node is not None:
input_schema = node.input 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
input_values = {k: k for k in self.builder.schemas[input_schema]}
is_single_input = len(input_values) == 1 and "__root__" in input_values
if input_schema in self.schema_to_mapper:
mapper = self.schema_to_mapper[input_schema]
else:
mapper = _pick_mapper(input_channels, input_schema)
mapper = _pick_mapper(
list(input_values),
input_schema,
)
self.schema_to_mapper[input_schema] = mapper
branch_channel = CHANNEL_BRANCH_TO.format(key)
@@ -1011,7 +1021,7 @@ class CompiledStateGraph(
self.nodes[key] = PregelNode(
triggers=[branch_channel],
# read state keys and managed values
channels=("__root__" if is_single_input else input_channels),
channels=(list(input_values) if is_single_input else input_values),
# coerce state dict to schema class (eg. pydantic model)
mapper=mapper,
# publish to state keys
+29 -22
View File
@@ -60,6 +60,7 @@ from langgraph.constants import (
NS_SEP,
NULL_TASK_ID,
PUSH,
SCHEDULED,
TASKS,
)
from langgraph.errors import (
@@ -144,7 +145,7 @@ class NodeBuilder:
"_cache_policy",
)
_channels: str | list[str]
_channels: list[str] | dict[str, str]
_triggers: list[str]
_tags: list[str]
_metadata: dict[str, Any]
@@ -156,7 +157,7 @@ class NodeBuilder:
def __init__(
self,
) -> None:
self._channels = []
self._channels = {}
self._triggers = []
self._tags = []
self._metadata = {}
@@ -170,8 +171,10 @@ class NodeBuilder:
channel: str,
) -> Self:
"""Subscribe to a single channel."""
if not self._channels:
self._channels = channel
if isinstance(self._channels, list):
self._channels.append(channel)
elif not self._channels:
self._channels = [channel]
else:
raise ValueError(
"Cannot subscribe to single channels when other channels are already subscribed to"
@@ -197,15 +200,15 @@ class NodeBuilder:
Returns:
Self for chaining
"""
if isinstance(self._channels, str):
if isinstance(self._channels, list):
raise ValueError(
"Cannot subscribe to channels when subscribed to a single channel"
)
if read:
if not self._channels:
self._channels = list(channels)
self._channels = {chan: chan for chan in channels}
else:
self._channels.extend(channels)
self._channels.update({chan: chan for chan in channels})
if isinstance(channels, str):
self._triggers.append(channels)
@@ -219,10 +222,11 @@ class NodeBuilder:
*channels: str,
) -> Self:
"""Adds the specified channels to read from, without subscribing to them."""
assert isinstance(self._channels, list), (
assert self._channels, "Channels must be specified first"
assert isinstance(self._channels, dict), (
"Cannot read additional channels when subscribed to single channels"
)
self._channels.extend(channels)
self._channels.update({c: c for c in channels})
return self
def do(
@@ -589,6 +593,8 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
config_type: type[Any] | None = None
input_model: type[BaseModel] | None = None
config: RunnableConfig | None = None
name: str = "LangGraph"
@@ -616,6 +622,7 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
retry_policy: RetryPolicy | Sequence[RetryPolicy] = (),
cache_policy: CachePolicy | None = None,
config_type: type[Any] | None = None,
input_model: type[BaseModel] | None = None,
config: RunnableConfig | None = None,
trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
name: str = "LangGraph",
@@ -647,6 +654,7 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
)
self.cache_policy = cache_policy
self.config_type = config_type
self.input_model = input_model
self.config = config
self.trigger_to_nodes = trigger_to_nodes or {}
self.name = name
@@ -745,7 +753,6 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
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,
@@ -784,6 +791,8 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
return channel.UpdateType
def get_input_schema(self, config: RunnableConfig | None = None) -> type[BaseModel]:
if self.input_model is not None:
return self.input_model
config = merge_configs(self.config, config)
if isinstance(self.input_channels, str):
return super().get_input_schema(config)
@@ -1002,7 +1011,7 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
)
if apply_pending_writes and saved.pending_writes:
for tid, k, v in saved.pending_writes:
if k in (ERROR, INTERRUPT):
if k in (ERROR, INTERRUPT, SCHEDULED):
continue
if tid not in next_tasks:
continue
@@ -1121,7 +1130,7 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
)
if apply_pending_writes and saved.pending_writes:
for tid, k, v in saved.pending_writes:
if k in (ERROR, INTERRUPT):
if k in (ERROR, INTERRUPT, SCHEDULED):
continue
if tid not in next_tasks:
continue
@@ -1460,7 +1469,7 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
)
# apply writes from tasks that already ran
for tid, k, v in saved.pending_writes or []:
if k in (ERROR, INTERRUPT):
if k in (ERROR, INTERRUPT, SCHEDULED):
continue
if tid not in next_tasks:
continue
@@ -1624,7 +1633,7 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
)
# apply writes
for tid, k, v in saved.pending_writes:
if k in (ERROR, INTERRUPT):
if k in (ERROR, INTERRUPT, SCHEDULED):
continue
if tid not in next_tasks:
continue
@@ -1880,7 +1889,7 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
)
# apply writes from tasks that already ran
for tid, k, v in saved.pending_writes or []:
if k in (ERROR, INTERRUPT):
if k in (ERROR, INTERRUPT, SCHEDULED):
continue
if tid not in next_tasks:
continue
@@ -2043,7 +2052,7 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
self.trigger_to_nodes,
)
for tid, k, v in saved.pending_writes:
if k in (ERROR, INTERRUPT):
if k in (ERROR, INTERRUPT, SCHEDULED):
continue
if tid not in next_tasks:
continue
@@ -2398,6 +2407,7 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
config[CONF][CONFIG_KEY_CHECKPOINT_DURING] = checkpoint_during
with SyncPregelLoop(
input,
input_model=self.input_model,
stream=StreamProtocol(stream.put, stream_modes),
config=config,
store=store,
@@ -2406,7 +2416,6 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
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_,
@@ -2461,7 +2470,7 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
# 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():
while loop.tick(input_keys=self.input_channels):
for task in loop.match_cached_writes():
loop.output_writes(task.id, task.writes, cached=True)
for _ in runner.tick(
@@ -2472,7 +2481,6 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
):
# emit output
yield from output()
loop.after_tick()
# emit output
yield from output()
# handle exit
@@ -2642,6 +2650,7 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
config[CONF][CONFIG_KEY_CHECKPOINT_DURING] = checkpoint_during
async with AsyncPregelLoop(
input,
input_model=self.input_model,
stream=StreamProtocol(stream.put_nowait, stream_modes),
config=config,
store=store,
@@ -2650,7 +2659,6 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
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_,
@@ -2696,7 +2704,7 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
# 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():
while loop.tick(input_keys=self.input_channels):
for task in await loop.amatch_cached_writes():
loop.output_writes(task.id, task.writes, cached=True)
async for _ in runner.atick(
@@ -2708,7 +2716,6 @@ class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, Ou
# emit output
for o in output():
yield o
loop.after_tick()
# emit output
for o in output():
yield o
+16 -13
View File
@@ -922,18 +922,18 @@ def _triggers(
seen: ChannelVersions | None,
null_version: V,
proc: PregelNode,
) -> bool:
) -> Sequence[str]:
if seen is None:
for chan in proc.triggers:
if channels[chan].is_available():
return True
return (chan,)
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
return (chan,)
return EMPTY_SEQ
def _scratchpad(
@@ -1019,20 +1019,23 @@ def _proc_input(
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):
if isinstance(proc.channels, dict):
val: dict[str, Any] = {}
for k, chan in proc.channels.items():
if chan in channels:
if channels[chan].is_available():
val[k] = channels[chan].get()
else:
val[k] = managed[k].get(scratchpad)
elif isinstance(proc.channels, list):
for chan in proc.channels:
if chan in channels:
if channels[chan].is_available():
val[chan] = channels[chan].get()
val = channels[chan].get()
break
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
val = managed[chan].get(scratchpad)
break
else:
return MISSING
else:
+202 -83
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
import binascii
import concurrent.futures
import dataclasses
from collections import defaultdict, deque
from collections.abc import Iterator, Mapping, Sequence
from contextlib import (
@@ -24,6 +25,7 @@ from typing import (
from langchain_core.callbacks import AsyncParentRunManager, ParentRunManager
from langchain_core.runnables import RunnableConfig
from pydantic import BaseModel
from typing_extensions import ParamSpec, Self
from langgraph.cache.base import BaseCache
@@ -44,6 +46,9 @@ from langgraph.constants import (
CONFIG_KEY_CHECKPOINT_ID,
CONFIG_KEY_CHECKPOINT_MAP,
CONFIG_KEY_CHECKPOINT_NS,
CONFIG_KEY_DEDUPE_TASKS,
CONFIG_KEY_DELEGATE,
CONFIG_KEY_ENSURE_LATEST,
CONFIG_KEY_RESUME_MAP,
CONFIG_KEY_RESUMING,
CONFIG_KEY_SCRATCHPAD,
@@ -55,15 +60,17 @@ from langgraph.constants import (
INPUT,
INTERRUPT,
MISSING,
NS_END,
NS_SEP,
NULL_TASK_ID,
PUSH,
RESUME,
SCHEDULED,
TAG_HIDDEN,
)
from langgraph.errors import (
CheckpointNotLatest,
EmptyInputError,
GraphDelegate,
GraphInterrupt,
)
from langgraph.managed.base import (
@@ -125,7 +132,9 @@ from langgraph.utils.config import patch_configurable
V = TypeVar("V")
P = ParamSpec("P")
INPUT_DONE = object()
INPUT_RESUMING = object()
INPUT_SHOULD_VALIDATE = object()
WritesT = Sequence[tuple[str, Any]]
@@ -146,11 +155,11 @@ class PregelLoop:
stop: int
input: Any | None
input_model: type[BaseModel] | 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
@@ -193,16 +202,11 @@ class PregelLoop:
prev_checkpoint_config: RunnableConfig | None
status: Literal[
"input",
"pending",
"done",
"interrupt_before",
"interrupt_after",
"out_of_steps",
"pending", "done", "interrupt_before", "interrupt_after", "out_of_steps"
]
tasks: dict[str, PregelExecutableTask]
to_interrupt: list[PregelExecutableTask]
output: None | dict[str, Any] | Any = None
updated_channels: set[str] | None = None
# public
@@ -217,13 +221,13 @@ class PregelLoop:
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]],
interrupt_after: All | Sequence[str] = EMPTY_SEQ,
interrupt_before: All | Sequence[str] = EMPTY_SEQ,
manager: None | AsyncParentRunManager | ParentRunManager = None,
input_model: type[BaseModel] | None = None,
debug: bool = False,
migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
retry_policy: Sequence[RetryPolicy] = (),
@@ -236,18 +240,21 @@ class PregelLoop:
self.step = 0
self.stop = 0
self.input = input
self.input_model = input_model
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.skip_done_tasks = (
CONFIG_KEY_CHECKPOINT_ID not in config[CONF]
or CONFIG_KEY_DEDUPE_TASKS in config[CONF]
)
self._migrate_checkpoint = migrate_checkpoint
self.trigger_to_nodes = trigger_to_nodes
self.retry_policy = retry_policy
@@ -257,7 +264,9 @@ class PregelLoop:
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 not self.config[CONF].get(CONFIG_KEY_DELEGATE) and isinstance(
scratchpad, PregelScratchpad
):
# if count is > 0, append to checkpoint_ns
# if count is 0, leave as is
if cnt := scratchpad.subgraph_counter():
@@ -395,6 +404,12 @@ class PregelLoop:
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."""
# don't start if we should interrupt *after* the original task
if self.interrupt_after and should_interrupt(
self.checkpoint, self.interrupt_after, [task]
):
self.to_interrupt.append(task)
return
checkpoint_id_bytes = binascii.unhexlify(self.checkpoint["id"].replace("-", ""))
null_version = checkpoint_null_version(self.checkpoint)
if pushed := cast(
@@ -420,6 +435,12 @@ class PregelLoop:
cache_policy=self.cache_policy,
),
):
# don't start if we should interrupt *before* the new task
if self.interrupt_before and should_interrupt(
self.checkpoint, self.interrupt_before, [pushed]
):
self.to_interrupt.append(pushed)
return
# produce debug output
self._emit("debug", map_debug_tasks, self.step, [pushed])
# debug flag
@@ -433,7 +454,11 @@ class PregelLoop:
# return the new task, to be started if not run before
return pushed
def tick(self) -> bool:
def tick(
self,
*,
input_keys: str | Sequence[str],
) -> bool:
"""Execute a single iteration of the Pregel loop.
Args:
@@ -442,6 +467,72 @@ class PregelLoop:
Returns:
True if more iterations are needed.
"""
if self.status != "pending":
raise RuntimeError("Cannot tick when status is no longer 'pending'")
updated_channels: set[str] | None = None
if self.input not in (INPUT_DONE, INPUT_RESUMING, INPUT_SHOULD_VALIDATE):
updated_channels = self._first(input_keys=input_keys)
elif self.to_interrupt:
# if we need to interrupt, do so
self.status = "interrupt_before"
raise GraphInterrupt()
elif all(task.writes for task in self.tasks.values()):
# finish superstep
writes = [w for t in self.tasks.values() for w in t.writes]
# debug flag
if self.debug:
print_step_writes(
self.step,
writes,
(
[self.stream_keys]
if isinstance(self.stream_keys, str)
else self.stream_keys
),
)
# all tasks have finished
updated_channels = apply_writes(
self.checkpoint,
self.channels,
self.tasks.values(),
self.checkpointer_get_next_version,
self.trigger_to_nodes,
)
# validate input if requested
if self.input is INPUT_SHOULD_VALIDATE:
self.input = INPUT_DONE
# validate
cast(type[BaseModel], self.input_model)(
**read_channels(self.channels, self.stream_keys)
)
# produce values output
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, 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)
else:
return False
# check if iteration limit is reached
if self.step > self.stop:
@@ -463,10 +554,11 @@ class PregelLoop:
store=self.store,
checkpointer=self.checkpointer,
trigger_to_nodes=self.trigger_to_nodes,
updated_channels=self.updated_channels,
updated_channels=updated_channels,
retry_policy=self.retry_policy,
cache_policy=self.cache_policy,
)
self.to_interrupt = []
# produce debug output
if self._checkpointer_put_after_previous is not None:
@@ -496,10 +588,26 @@ class PregelLoop:
self.status = "done"
return False
# check if we should delegate (used by subgraphs in distributed mode)
if self.config[CONF].get(CONFIG_KEY_DELEGATE):
assert self.input is INPUT_RESUMING
raise GraphDelegate(
{
"config": patch_configurable(
self.config, {CONFIG_KEY_DELEGATE: False}
),
"input": None,
}
)
# 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)
# if all tasks have finished, re-tick
if all(task.writes for task in self.tasks.values()):
return self.tick(input_keys=input_keys)
# before execution, check if we should interrupt
if self.interrupt_before and should_interrupt(
self.checkpoint, self.interrupt_before, self.tasks.values()
@@ -521,52 +629,6 @@ class PregelLoop:
return True
def after_tick(self) -> None:
# finish superstep
writes = [w for t in self.tasks.values() for w in t.writes]
# debug flag
if self.debug:
print_step_writes(
self.step,
writes,
(
[self.stream_keys]
if isinstance(self.stream_keys, str)
else self.stream_keys
),
)
# 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
@@ -580,7 +642,14 @@ class PregelLoop:
if k in (ERROR, INTERRUPT, RESUME):
continue
if task := tasks.get(tid):
task.writes.append((k, v))
if k == SCHEDULED:
if v == max(
self.checkpoint["versions_seen"].get(INTERRUPT, {}).values(),
default=None,
):
self.tasks[tid] = dataclasses.replace(task, scheduled=True)
else:
task.writes.append((k, v))
def _first(self, *, input_keys: str | Sequence[str]) -> set[str] | None:
# resuming from previous checkpoint requires
@@ -646,8 +715,21 @@ class PregelLoop:
self._emit(
"values", map_output_values, self.output_keys, True, self.channels
)
# set flag
self.input = INPUT_RESUMING
# map inputs to channel updates
elif input_writes := deque(map_input(input_keys, self.input)):
# TODO shouldn't these writes be passed to put_writes too?
# check if we should delegate (used by subgraphs in distributed mode)
if self.config[CONF].get(CONFIG_KEY_DELEGATE):
raise GraphDelegate(
{
"config": patch_configurable(
self.config, {CONFIG_KEY_DELEGATE: False}
),
"input": self.input,
}
)
# discard any unfinished tasks from previous checkpoint
discard_tasks = prepare_next_tasks(
self.checkpoint,
@@ -676,15 +758,24 @@ class PregelLoop:
)
# save input checkpoint
self._put_checkpoint({"source": "input"})
# set flag
if (
self.input_model is not None
and not isinstance(self.input, self.input_model)
and not isinstance(self.stream_keys, str)
):
self.input = INPUT_SHOULD_VALIDATE
else:
self.input = INPUT_DONE
elif CONFIG_KEY_RESUMING not in configurable:
raise EmptyInputError(f"Received no input for {input_keys}")
else:
self.input = INPUT_DONE
# 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:
@@ -777,14 +868,7 @@ class PregelLoop:
traceback: TracebackType | None,
) -> bool | None:
# persist current checkpoint and writes
if not self.checkpoint_during 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)
):
if not self.checkpoint_during:
self._put_checkpoint(self.checkpoint_metadata)
self._put_pending_writes()
# suppress interrupt
@@ -908,9 +992,9 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
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,
input_model: type[BaseModel] | None = None,
debug: bool = False,
migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
retry_policy: Sequence[RetryPolicy] = (),
@@ -919,6 +1003,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
) -> None:
super().__init__(
input,
input_model=input_model,
stream=stream,
config=config,
checkpointer=checkpointer,
@@ -926,7 +1011,6 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
store=store,
nodes=nodes,
specs=specs,
input_keys=input_keys,
output_keys=output_keys,
stream_keys=stream_keys,
interrupt_after=interrupt_after,
@@ -1013,7 +1097,25 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
# context manager
def __enter__(self) -> Self:
if self.checkpointer:
if self.config.get(CONF, {}).get(
CONFIG_KEY_ENSURE_LATEST
) and self.checkpoint_config[CONF].get(CONFIG_KEY_CHECKPOINT_ID):
if self.checkpointer is None:
raise RuntimeError(
"Cannot ensure latest checkpoint without checkpointer"
)
saved = self.checkpointer.get_tuple(
patch_configurable(
self.checkpoint_config, {CONFIG_KEY_CHECKPOINT_ID: None}
)
)
if (
saved is None
or saved.checkpoint["id"]
!= self.checkpoint_config[CONF][CONFIG_KEY_CHECKPOINT_ID]
):
raise CheckpointNotLatest
elif self.checkpointer:
saved = self.checkpointer.get_tuple(self.checkpoint_config)
else:
saved = None
@@ -1047,11 +1149,10 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
self.specs, self.checkpoint
)
self.stack.push(self._suppress_interrupt)
self.status = "input"
self.status = "pending"
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)
return self
@@ -1081,9 +1182,9 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
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,
input_model: type[BaseModel] | None = None,
debug: bool = False,
migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
retry_policy: Sequence[RetryPolicy] = (),
@@ -1092,6 +1193,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
) -> None:
super().__init__(
input,
input_model=input_model,
stream=stream,
config=config,
checkpointer=checkpointer,
@@ -1099,7 +1201,6 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
store=store,
nodes=nodes,
specs=specs,
input_keys=input_keys,
output_keys=output_keys,
stream_keys=stream_keys,
interrupt_after=interrupt_after,
@@ -1189,7 +1290,25 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
# context manager
async def __aenter__(self) -> Self:
if self.checkpointer:
if self.config.get(CONF, {}).get(
CONFIG_KEY_ENSURE_LATEST
) and self.checkpoint_config[CONF].get(CONFIG_KEY_CHECKPOINT_ID):
if self.checkpointer is None:
raise RuntimeError(
"Cannot ensure latest checkpoint without checkpointer"
)
saved = await self.checkpointer.aget_tuple(
patch_configurable(
self.checkpoint_config, {CONFIG_KEY_CHECKPOINT_ID: None}
)
)
if (
saved is None
or saved.checkpoint["id"]
!= self.checkpoint_config[CONF][CONFIG_KEY_CHECKPOINT_ID]
):
raise CheckpointNotLatest
elif self.checkpointer:
saved = await self.checkpointer.aget_tuple(self.checkpoint_config)
else:
saved = None
@@ -1225,11 +1344,11 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
self.specs, self.checkpoint
)
self.stack.push(self._suppress_interrupt)
self.status = "input"
self.status = "pending"
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)
return self
+60 -10
View File
@@ -12,11 +12,12 @@ from langchain_core.runnables import Runnable, RunnableConfig
from langgraph.constants import CONF, CONFIG_KEY_READ
from langgraph.pregel.protocol import PregelProtocol
from langgraph.pregel.retry import RetryPolicy
from langgraph.pregel.utils import find_subgraph_pregel
from langgraph.pregel.write import ChannelWrite
from langgraph.types import CachePolicy, RetryPolicy
from langgraph.types import CachePolicy
from langgraph.utils.config import merge_configs
from langgraph.utils.runnable import RunnableCallable, RunnableSeq
from langgraph.utils.runnable import RunnableCallable, RunnableSeq, coerce_to_runnable
READ_TYPE = Callable[[Union[str, Sequence[str]], bool], Union[Any, dict[str, Any]]]
INPUT_CACHE_KEY_TYPE = tuple[Callable[..., Any], tuple[str, ...]]
@@ -95,15 +96,16 @@ class ChannelRead(RunnableCallable):
DEFAULT_BOUND = RunnableCallable(lambda input: input)
class PregelNode:
class PregelNode(Runnable):
"""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]
channels: list[str] | Mapping[str, 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."""
If a list, the node will be invoked with the first of that isn't empty.
If a dict, the keys are the names of the channels, and the values are the keys
to use in the input to `bound`."""
triggers: list[str]
"""If any of these channels is written to, this node will be triggered in
@@ -138,7 +140,7 @@ class PregelNode:
def __init__(
self,
*,
channels: str | list[str],
channels: list[str] | Mapping[str, str],
triggers: Sequence[str],
mapper: Callable[[Any], Any] | None = None,
writers: list[Runnable] | None = None,
@@ -221,11 +223,59 @@ class PregelNode:
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,),
tuple(f"{key}:{value}" for key, value in self.channels.items())
if isinstance(self.channels, dict)
else tuple(self.channels),
)
def join(self, channels: Sequence[str]) -> PregelNode:
assert isinstance(channels, list) or isinstance(channels, tuple), (
"channels must be a list or tuple"
)
assert isinstance(self.channels, dict), (
"all channels must be named when using .join()"
)
return self.copy(
update=dict(
channels={
**self.channels,
**{chan: chan for chan in channels},
}
),
)
def __or__(
self,
other: Runnable[Any, Any]
| Callable[[Any], Any]
| Mapping[str, Runnable[Any, Any] | Callable[[Any], Any]],
) -> PregelNode:
if isinstance(other, Runnable) and ChannelWrite.is_writer(other):
return self.copy(update=dict(writers=[*self.writers, other]))
elif self.bound is DEFAULT_BOUND:
return self.copy(
update=dict(bound=coerce_to_runnable(other, name=None, trace=True))
)
else:
return self.copy(update=dict(bound=RunnableSeq(self.bound, other)))
def pipe(
self,
*others: Runnable[Any, Any] | Callable[[Any], Any],
name: str | None = None,
) -> PregelNode:
for other in others:
self = self | other
return self
def __ror__(
self,
other: Runnable[Any, Any]
| Callable[[Any], Any]
| Mapping[str, Runnable[Any, Any] | Callable[[Any], Any]],
) -> PregelNode:
raise NotImplementedError()
def invoke(
self,
input: Any,
+2 -20
View File
@@ -5,7 +5,6 @@ from typing import Any
from langgraph.channels.base import BaseChannel
from langgraph.constants import RESERVED
from langgraph.managed.base import ManagedValueMapping
from langgraph.pregel.read import PregelNode
from langgraph.types import All
@@ -13,7 +12,6 @@ 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,
@@ -22,30 +20,14 @@ def validate_graph(
) -> 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")
raise ValueError(f"Channel names {chan} are reserved")
subscribed_channels = set[str]()
for name, node in nodes.items():
if name in RESERVED:
raise ValueError(f"Node name '{name}' is reserved")
raise ValueError(f"Node names {RESERVED} are 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"
+1
View File
@@ -203,6 +203,7 @@ class PregelExecutableTask:
cache_key: CacheKey | None
id: str
path: tuple[str | int | tuple, ...]
scheduled: bool = False
writers: Sequence[Runnable] = ()
subgraphs: Sequence[PregelProtocol] = ()
+94 -2
View File
@@ -4213,6 +4213,44 @@ def test_doubly_nested_graph_state(
# get child graph history
child_history = list(app.get_state_history(outer_history[1].tasks[0].state))
assert child_history == [
StateSnapshot(
values={"my_key": "hi my value here and there"},
next=(),
config={
"configurable": {
"thread_id": "1",
"checkpoint_ns": AnyStr("child:"),
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict(
{"": AnyStr(), AnyStr("child:"): AnyStr()}
),
}
},
metadata={
"source": "loop",
"step": 1,
"parents": {"": AnyStr()},
"thread_id": "1",
"langgraph_node": "child",
"langgraph_path": [PULL, AnyStr("child")],
"langgraph_step": 2,
"langgraph_triggers": ["branch:to:child"],
"langgraph_checkpoint_ns": AnyStr("child:"),
},
created_at=AnyStr(),
parent_config={
"configurable": {
"thread_id": "1",
"checkpoint_ns": AnyStr("child:"),
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict(
{"": AnyStr(), AnyStr("child:"): AnyStr()}
),
}
},
tasks=(),
interrupts=(),
),
StateSnapshot(
values={"my_key": "hi my value"},
next=("child_1",),
@@ -4257,8 +4295,62 @@ def test_doubly_nested_graph_state(
),
]
# get grandchild graph history
grandchild_history = list(app.get_state_history(child_history[0].tasks[0].state))
grandchild_history = list(app.get_state_history(child_history[1].tasks[0].state))
assert grandchild_history == [
StateSnapshot(
values={"my_key": "hi my value here and there"},
next=(),
config={
"configurable": {
"thread_id": "1",
"checkpoint_ns": AnyStr(),
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict(
{
"": AnyStr(),
AnyStr("child:"): AnyStr(),
AnyStr(re.compile(r"child:.+|child1:")): AnyStr(),
}
),
}
},
metadata={
"source": "loop",
"step": 2,
"parents": AnyDict(
{
"": AnyStr(),
AnyStr("child:"): AnyStr(),
}
),
"thread_id": "1",
"langgraph_checkpoint_ns": AnyStr("child:"),
"langgraph_node": "child_1",
"langgraph_path": [
PULL,
AnyStr("child_1"),
],
"langgraph_step": 1,
"langgraph_triggers": ["branch:to:child_1"],
},
created_at=AnyStr(),
parent_config={
"configurable": {
"thread_id": "1",
"checkpoint_ns": AnyStr(),
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict(
{
"": AnyStr(),
AnyStr("child:"): AnyStr(),
AnyStr(re.compile(r"child:.+|child1:")): AnyStr(),
}
),
}
},
tasks=(),
interrupts=(),
),
StateSnapshot(
values={"my_key": "hi my value here"},
next=("grandchild_2",),
@@ -4326,7 +4418,7 @@ def test_send_to_nested_graphs(sync_checkpointer: BaseCheckpointSaver) -> None:
return {"subject": f"{subject} - hohoho"}
# subgraph
subgraph = StateGraph(JokeState, output_schema=OverallState)
subgraph = StateGraph(JokeState, output=OverallState)
subgraph.add_node("edit", edit)
subgraph.add_node(
"generate", lambda state: {"jokes": [f"Joke about {state['subject']}"]}
+96 -2
View File
@@ -3028,6 +3028,44 @@ async def test_doubly_nested_graph_state(
c async for c in app.aget_state_history(outer_history[1].tasks[0].state)
]
assert child_history == [
StateSnapshot(
values={"my_key": "hi my value here and there"},
next=(),
config={
"configurable": {
"thread_id": "1",
"checkpoint_ns": AnyStr("child:"),
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict(
{"": AnyStr(), AnyStr("child:"): AnyStr()}
),
}
},
metadata={
"source": "loop",
"step": 1,
"parents": {"": AnyStr()},
"thread_id": "1",
"langgraph_node": "child",
"langgraph_path": [PULL, AnyStr("child")],
"langgraph_step": 2,
"langgraph_triggers": ["branch:to:child"],
"langgraph_checkpoint_ns": AnyStr("child:"),
},
created_at=AnyStr(),
parent_config={
"configurable": {
"thread_id": "1",
"checkpoint_ns": AnyStr("child:"),
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict(
{"": AnyStr(), AnyStr("child:"): AnyStr()}
),
}
},
tasks=(),
interrupts=(),
),
StateSnapshot(
values={"my_key": "hi my value"},
next=("child_1",),
@@ -3073,9 +3111,65 @@ async def test_doubly_nested_graph_state(
]
# get grandchild graph history
grandchild_history = [
c async for c in app.aget_state_history(child_history[0].tasks[0].state)
c async for c in app.aget_state_history(child_history[1].tasks[0].state)
]
assert grandchild_history == [
StateSnapshot(
values={"my_key": "hi my value here and there"},
next=(),
config={
"configurable": {
"thread_id": "1",
"checkpoint_ns": AnyStr(),
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict(
{
"": AnyStr(),
AnyStr("child:"): AnyStr(),
AnyStr(re.compile(r"child:.+|child1:")): AnyStr(),
}
),
}
},
metadata={
"source": "loop",
"step": 2,
"parents": AnyDict(
{
"": AnyStr(),
AnyStr("child:"): AnyStr(),
}
),
"thread_id": "1",
"langgraph_checkpoint_ns": AnyStr("child:"),
"langgraph_node": "child_1",
"langgraph_path": [
PULL,
AnyStr("child_1"),
],
"langgraph_step": 1,
"langgraph_triggers": [
"branch:to:child_1",
],
},
created_at=AnyStr(),
parent_config={
"configurable": {
"thread_id": "1",
"checkpoint_ns": AnyStr(),
"checkpoint_id": AnyStr(),
"checkpoint_map": AnyDict(
{
"": AnyStr(),
AnyStr("child:"): AnyStr(),
AnyStr(re.compile(r"child:.+|child1:")): AnyStr(),
}
),
}
},
tasks=(),
interrupts=(),
),
StateSnapshot(
values={"my_key": "hi my value here"},
next=("grandchild_2",),
@@ -3145,7 +3239,7 @@ async def test_send_to_nested_graphs(async_checkpointer: BaseCheckpointSaver) ->
return {"subject": f"{subject} - hohoho"}
# subgraph
subgraph = StateGraph(JokeState, output_schema=OverallState)
subgraph = StateGraph(JokeState, output=OverallState)
subgraph.add_node("edit", edit)
subgraph.add_node(
"generate", lambda state: {"jokes": [f"Joke about {state['subject']}"]}
+3 -57
View File
@@ -3276,57 +3276,6 @@ def test_subgraph_checkpoint_true(
),
]
checkpoints = list(app.get_state_history(config))
if checkpoint_during:
assert len(checkpoints) == 4
else:
assert len(checkpoints) == 1
def test_subgraph_checkpoint_during_false_inherited() -> None:
sync_checkpointer = InMemorySaver()
class InnerState(TypedDict):
my_key: Annotated[str, operator.add]
my_other_key: str
def inner_1(state: InnerState):
return {"my_key": " got here", "my_other_key": state["my_key"]}
def inner_2(state: InnerState):
return {"my_key": " and there"}
inner = StateGraph(InnerState)
inner.add_node("inner_1", inner_1)
inner.add_node("inner_2", inner_2)
inner.add_edge("inner_1", "inner_2")
inner.set_entry_point("inner_1")
inner.set_finish_point("inner_2")
class State(TypedDict):
my_key: str
inner_app = inner.compile(checkpointer=sync_checkpointer)
graph = StateGraph(State)
graph.add_node("inner", inner_app)
graph.add_edge(START, "inner")
graph.add_conditional_edges(
"inner", lambda s: "inner" if s["my_key"].count("there") < 2 else END
)
app = graph.compile(checkpointer=sync_checkpointer)
for checkpoint_during in [True, False]:
thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
app.invoke(
{"my_key": ""}, config, subgraphs=True, checkpoint_during=checkpoint_during
)
if checkpoint_during:
checkpoints = list(sync_checkpointer.list(config))
assert len(checkpoints) == 12
else:
checkpoints = list(sync_checkpointer.list(config))
assert len(checkpoints) == 1
def test_subgraph_checkpoint_true_interrupt(
sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool
@@ -4351,7 +4300,7 @@ def test_store_injected(
builder = StateGraph(State)
builder.add_node("node", Node())
builder.add_edge("__start__", "node")
N = 50
N = 500
M = 1
for i in range(N):
@@ -4626,14 +4575,11 @@ def test_debug_nested_subgraphs(
return clean_config
for checkpoint_events, checkpoint_history, ns in zip(
stream_ns.values(), history_ns.values(), stream_ns.keys()
for checkpoint_events, checkpoint_history in zip(
stream_ns.values(), history_ns.values()
):
if not checkpoint_during:
checkpoint_events = checkpoint_events[-1:]
if ns: # Save no checkpoints for subgraphs when checkpoint_during=False
assert not checkpoint_history
continue
assert len(checkpoint_events) == len(checkpoint_history)
for stream, history in zip(checkpoint_events, checkpoint_history):
assert stream["values"] == history.values
+13 -67
View File
@@ -1338,7 +1338,7 @@ async def test_node_schemas_custom_output() -> None:
"now": 123,
}
builder = StateGraph(State, output_schema=Output)
builder = StateGraph(State, output=Output)
builder.add_node("a", node_a)
builder.add_node("b", node_b)
builder.add_node("c", node_c)
@@ -1353,7 +1353,7 @@ async def test_node_schemas_custom_output() -> None:
"messages": [_AnyIdHumanMessage(content="hello")],
}
builder = StateGraph(State, output_schema=Output)
builder = StateGraph(State, output=Output)
builder.add_node("a", node_a)
builder.add_node("b", node_b)
builder.add_node("c", node_c)
@@ -5029,51 +5029,6 @@ async def test_subgraph_checkpoint_true(
]
async def test_subgraph_checkpoint_during_false_inherited() -> None:
async_checkpointer = InMemorySaver()
class InnerState(TypedDict):
my_key: Annotated[str, operator.add]
my_other_key: str
def inner_1(state: InnerState):
return {"my_key": " got here", "my_other_key": state["my_key"]}
def inner_2(state: InnerState):
return {"my_key": " and there"}
inner = StateGraph(InnerState)
inner.add_node("inner_1", inner_1)
inner.add_node("inner_2", inner_2)
inner.add_edge("inner_1", "inner_2")
inner.set_entry_point("inner_1")
inner.set_finish_point("inner_2")
class State(TypedDict):
my_key: str
inner_app = inner.compile(checkpointer=async_checkpointer)
graph = StateGraph(State)
graph.add_node("inner", inner_app)
graph.add_edge(START, "inner")
graph.add_conditional_edges(
"inner", lambda s: "inner" if s["my_key"].count("there") < 2 else END
)
app = graph.compile(checkpointer=async_checkpointer)
for checkpoint_during in [True, False]:
thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
await app.ainvoke(
{"my_key": ""}, config, subgraphs=True, checkpoint_during=checkpoint_during
)
if checkpoint_during:
checkpoints = list(async_checkpointer.list(config))
assert len(checkpoints) == 12
else:
checkpoints = list(async_checkpointer.list(config))
assert len(checkpoints) == 1
@NEEDS_CONTEXTVARS
async def test_subgraph_checkpoint_true_interrupt(
async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool
@@ -5781,7 +5736,7 @@ async def test_store_injected_async(
builder.add_edge("__start__", "node")
builder.add_edge("node", "other_node")
N = 50
N = 500
M = 1
for i in range(N):
@@ -6052,14 +6007,11 @@ async def test_debug_nested_subgraphs(
return clean_config
for checkpoint_events, checkpoint_history, ns in zip(
stream_ns.values(), history_ns.values(), stream_ns.keys()
for checkpoint_events, checkpoint_history in zip(
stream_ns.values(), history_ns.values()
):
if not checkpoint_during:
checkpoint_events = checkpoint_events[-1:]
if ns: # Save no checkpoints for subgraphs when checkpoint_during=False
assert not checkpoint_history
continue
assert len(checkpoint_events) == len(checkpoint_history)
for stream, history in zip(checkpoint_events, checkpoint_history):
assert stream["values"] == history.values
@@ -7030,17 +6982,14 @@ async def test_multiple_subgraphs(async_checkpointer: BaseCheckpointSaver) -> No
return {"result": state["a"] + state["b"]}
add_subgraph = (
StateGraph(State, output_schema=Output)
.add_node(add)
.add_edge(START, "add")
.compile()
StateGraph(State, output=Output).add_node(add).add_edge(START, "add").compile()
)
async def multiply(state):
return {"result": state["a"] * state["b"]}
multiply_subgraph = (
StateGraph(State, output_schema=Output)
StateGraph(State, output=Output)
.add_node(multiply)
.add_edge(START, "multiply")
.compile()
@@ -7053,7 +7002,7 @@ async def test_multiple_subgraphs(async_checkpointer: BaseCheckpointSaver) -> No
return another_result
parent_call_same_subgraph = (
StateGraph(State, output_schema=Output)
StateGraph(State, output=Output)
.add_node(call_same_subgraph)
.add_edge(START, "call_same_subgraph")
.compile(checkpointer=async_checkpointer)
@@ -7077,7 +7026,7 @@ async def test_multiple_subgraphs(async_checkpointer: BaseCheckpointSaver) -> No
}
parent_call_multiple_subgraphs = (
StateGraph(State, output_schema=Output)
StateGraph(State, output=Output)
.add_node(call_multiple_subgraphs)
.add_edge(START, "call_multiple_subgraphs")
.compile(checkpointer=async_checkpointer)
@@ -7155,17 +7104,14 @@ async def test_multiple_subgraphs_mixed_entrypoint(
return {"result": state["a"] + state["b"]}
add_subgraph = (
StateGraph(State, output_schema=Output)
.add_node(add)
.add_edge(START, "add")
.compile()
StateGraph(State, output=Output).add_node(add).add_edge(START, "add").compile()
)
async def multiply(state):
return {"result": state["a"] * state["b"]}
multiply_subgraph = (
StateGraph(State, output_schema=Output)
StateGraph(State, output=Output)
.add_node(multiply)
.add_edge(START, "multiply")
.compile()
@@ -7235,7 +7181,7 @@ async def test_multiple_subgraphs_mixed_state_graph(
return {"result": another_result}
parent_call_same_subgraph = (
StateGraph(State, output_schema=Output)
StateGraph(State, output=Output)
.add_node(call_same_subgraph)
.add_edge(START, "call_same_subgraph")
.compile(checkpointer=async_checkpointer)
@@ -7259,7 +7205,7 @@ async def test_multiple_subgraphs_mixed_state_graph(
}
parent_call_multiple_subgraphs = (
StateGraph(State, output_schema=Output)
StateGraph(State, output=Output)
.add_node(call_multiple_subgraphs)
.add_edge(START, "call_multiple_subgraphs")
.compile(checkpointer=async_checkpointer)
+1 -1
View File
@@ -92,7 +92,7 @@ def test_state_schema_with_type_hint():
assert state.pop("foo") == "bar"
return {"input_state": state}
graph = StateGraph(InputState, output_schema=OutputState)
graph = StateGraph(InputState, output=OutputState)
actions = [
complete_hint,
miss_first_hint,
+1586 -1588
View File
File diff suppressed because it is too large Load Diff
@@ -850,7 +850,7 @@ def _get_store_arg(tool: BaseTool) -> Optional[str]:
if _is_injection(type_arg, InjectedStore)
]
if len(injections) > 1:
raise ValueError(
ValueError(
"A tool argument should not be annotated with InjectedStore more than "
f"once. Received arg {name} with annotations {injections}."
)
+2 -5
View File
@@ -1089,17 +1089,14 @@ def test_react_with_subgraph_tools(
return {"result": state["a"] + state["b"]}
add_subgraph = (
StateGraph(State, output_schema=Output)
.add_node(add)
.add_edge(START, "add")
.compile()
StateGraph(State, output=Output).add_node(add).add_edge(START, "add").compile()
)
def multiply(state):
return {"result": state["a"] * state["b"]}
multiply_subgraph = (
StateGraph(State, output_schema=Output)
StateGraph(State, output=Output)
.add_node(multiply)
.add_edge(START, "multiply")
.compile()
+743 -745
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
"version": "0.0.84",
"version": "0.0.83",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
+9 -76
View File
@@ -39,75 +39,6 @@ import { getEnvironmentVariable } from "./utils/env.js";
import { mergeSignals } from "./utils/signals.js";
import { BytesLineDecoder, SSEDecoder } from "./utils/sse.js";
import { IterableReadableStream } from "./utils/stream.js";
type HeaderValue = string | undefined | null;
function* iterateHeaders(
headers: HeadersInit | Record<string, HeaderValue>,
): IterableIterator<[string, string | null]> {
let iter: Iterable<(HeaderValue | HeaderValue | null[])[]>;
let shouldClear = false;
if (headers instanceof Headers) {
const entries: [string, string][] = [];
headers.forEach((value, name) => {
entries.push([name, value]);
});
iter = entries;
} else if (Array.isArray(headers)) {
iter = headers;
} else {
shouldClear = true;
iter = Object.entries(headers ?? {});
}
for (let item of iter) {
const name = item[0];
if (typeof name !== "string")
throw new TypeError(
`Expected header name to be a string, got ${typeof name}`,
);
const values = Array.isArray(item[1]) ? item[1] : [item[1]];
let didClear = false;
for (const value of values) {
if (value === undefined) continue;
// New object keys should always overwrite older headers
// Yield a null to clear the header in the headers object
// before adding the new value
if (shouldClear && !didClear) {
didClear = true;
yield [name, null];
}
yield [name, value];
}
}
}
function mergeHeaders(
...headerObjects: (
| HeadersInit
| Record<string, HeaderValue>
| undefined
| null
)[]
) {
const outputHeaders = new Headers();
for (const headers of headerObjects) {
if (!headers) continue;
for (const [name, value] of iterateHeaders(headers)) {
if (value === null) outputHeaders.delete(name);
else outputHeaders.append(name, value);
}
}
const headerEntries: [string, string][] = [];
outputHeaders.forEach((value, name) => {
headerEntries.push([name, value]);
});
return Object.fromEntries(headerEntries);
}
/**
* Get the API key from the environment.
* Precedence:
@@ -165,7 +96,7 @@ export interface ClientConfig {
apiKey?: string;
callerOptions?: AsyncCallerParams;
timeoutMs?: number;
defaultHeaders?: Record<string, HeaderValue>;
defaultHeaders?: Record<string, string | null | undefined>;
onRequest?: RequestHook;
}
@@ -176,7 +107,7 @@ class BaseClient {
protected apiUrl: string;
protected defaultHeaders: Record<string, HeaderValue>;
protected defaultHeaders: Record<string, string | null | undefined>;
protected onRequest?: RequestHook;
@@ -216,7 +147,7 @@ class BaseClient {
this.onRequest = config?.onRequest;
const apiKey = getApiKey(config?.apiKey);
if (apiKey) {
this.defaultHeaders["x-api-key"] = apiKey;
this.defaultHeaders["X-Api-Key"] = apiKey;
}
}
@@ -231,14 +162,15 @@ class BaseClient {
): [url: URL, init: RequestInit] {
const mutatedOptions = {
...options,
headers: mergeHeaders(this.defaultHeaders, options?.headers),
headers: { ...this.defaultHeaders, ...options?.headers },
};
if (mutatedOptions.json) {
mutatedOptions.body = JSON.stringify(mutatedOptions.json);
mutatedOptions.headers = mergeHeaders(mutatedOptions.headers, {
"content-type": "application/json",
});
mutatedOptions.headers = {
...mutatedOptions.headers,
"Content-Type": "application/json",
};
delete mutatedOptions.json;
}
@@ -761,6 +693,7 @@ export class ThreadsClient<
offset?: number;
/**
* Thread status to filter on.
* Must be one of 'idle', 'busy', 'interrupted' or 'error'.
*/
status?: ThreadStatus;
/**
-123
View File
@@ -74,128 +74,5 @@ describe.each([["global"], ["mocked"]])(
expect(unexpectedFetchMock).not.toHaveBeenCalled();
});
});
describe("header coalescing", () => {
it("should properly merge headers with conflicting name casing", async () => {
const client = new Client({ apiKey: "test-api-key" });
await (client.threads as any).fetch("/test", {
headers: { "X-Api-Key": "custom-value" },
});
expect(expectedFetchMock).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
headers: expect.objectContaining({
"x-api-key": "custom-value",
}),
}),
);
});
it("should properly merge headers from multiple sources", async () => {
const client = new Client({
apiKey: "test-api-key",
defaultHeaders: {
"x-default": "default-value",
"x-override": "default-value",
},
});
await (client.threads as any).fetch("/test", {
headers: {
"x-custom": "custom-value",
"x-override": "custom-value",
},
});
expect(expectedFetchMock).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
headers: expect.objectContaining({
"x-api-key": "test-api-key",
"x-default": "default-value",
"x-custom": "custom-value",
"x-override": "custom-value",
}),
}),
);
vi.clearAllMocks();
// Test with null/undefined values
await (client.threads as any).fetch("/test", {
headers: {
"x-null": null,
"x-undefined": undefined,
"x-empty": "",
},
});
expect(expectedFetchMock).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
headers: expect.objectContaining({
"x-api-key": "test-api-key",
"x-default": "default-value",
}),
}),
);
expect(expectedFetchMock).not.toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
headers: expect.objectContaining({
"x-null": null,
"x-undefined": undefined,
}),
}),
);
});
it("should handle Headers object input", async () => {
const client = new Client({ apiKey: "test-api-key" });
const headers = new Headers();
headers.append("x-custom", "custom-value");
headers.append("x-multi", "value1");
headers.append("x-multi", "value2");
await (client.threads as any).fetch("/test", { headers });
expect(expectedFetchMock).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
headers: expect.objectContaining({
"x-api-key": "test-api-key",
"x-custom": "custom-value",
"x-multi": "value1, value2",
}),
}),
);
});
it("should handle array of header tuples", async () => {
const client = new Client({
apiKey: "test-api-key",
defaultHeaders: {
"x-custom": "custom-value",
},
});
const headers = [
["x-multi", "value1"],
["x-multi", "value2"],
];
await (client.threads as any).fetch("/test", { headers });
expect(expectedFetchMock).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
headers: expect.objectContaining({
"x-api-key": "test-api-key",
"x-custom": "custom-value",
"x-multi": "value1, value2",
}),
}),
);
});
});
},
);
-9
View File
@@ -70,15 +70,6 @@ export type ToolMessage = {
tool_call_id: string;
additional_kwargs?: MessageAdditionalKwargs | undefined;
response_metadata?: Record<string, unknown> | undefined;
/**
* Artifact of the Tool execution which is not meant to be sent to the model.
*
* Should only be specified if it is different from the message content, e.g. if only
* a subset of the full tool output is being passed as message content but the full
* output is needed in other parts of the code.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
artifact?: any;
};
export type SystemMessage = {