mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-18 21:55:46 +02:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05e4efe712 | ||
|
|
c80d93c78a | ||
|
|
87cb509528 | ||
|
|
84023451a2 | ||
|
|
efe78447b3 | ||
|
|
df19173191 | ||
|
|
4d01e69b82 | ||
|
|
e86b5f4da2 | ||
|
|
b70d5aac0e | ||
|
|
297242913f | ||
|
|
02965fb5f5 | ||
|
|
64f0458296 | ||
|
|
76711536f9 | ||
|
|
6c6978918e |
@@ -0,0 +1,6 @@
|
||||
# Contributing to LangGraph
|
||||
|
||||
Hi there! Thank you for even being interested in contributing to LangGraph.
|
||||
As an open-source project in a rapidly developing field, we are extremely open to contributions, whether they involve new features, improved infrastructure, better documentation, or bug fixes.
|
||||
|
||||
To learn how to contribute to LangGraph, please follow the [contribution guide here](https://docs.langchain.com/oss/python/contributing).
|
||||
@@ -0,0 +1,55 @@
|
||||
# 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 Server 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.
|
||||
-293
@@ -1,293 +0,0 @@
|
||||
# Contributing to LangGraph
|
||||
|
||||
Thank you for being interested in contributing to LangGraph!
|
||||
|
||||
## General guidelines
|
||||
|
||||
Here are some things to keep in mind for all types of contributions:
|
||||
|
||||
- Follow the ["fork and pull request"](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project) workflow.
|
||||
- Fill out the checked-in pull request template when opening pull requests. Note related issues and tag relevant maintainers.
|
||||
- Ensure your PR passes formatting, linting, and testing checks before requesting a review.
|
||||
- If you would like comments or feedback, please tag a maintainer.
|
||||
- Backwards compatibility is key. Your changes must not be breaking, except in case of critical bug and security fixes.
|
||||
- Look for duplicate PRs or issues that have already been opened before opening a new one.
|
||||
- Keep scope as isolated as possible. As a general rule, your changes should not affect more than one package at a time.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
For bug fixes, please open up an issue before proposing a fix to ensure the proposal properly addresses the underlying problem. In general, bug fixes should all have an accompanying unit test that fails before the fix.
|
||||
|
||||
### New features
|
||||
|
||||
For new features, please start a new [discussion](https://forum.langchain.com/), where the maintainers will help with scoping out the necessary changes.
|
||||
|
||||
## Contribute Documentation
|
||||
|
||||
Documentation is a vital part of LangGraph. We welcome both new documentation for new features and
|
||||
community improvements to our current documentation. Please read the resources below before getting started:
|
||||
|
||||
- [Documentation style guide](#documentation-style-guide)
|
||||
- [Documentation setup](#setup)
|
||||
|
||||
## Documentation Style Guide
|
||||
|
||||
As LangGraph continues to grow, the surface area of documentation required to cover it continues to grow too.
|
||||
This page provides guidelines for anyone writing documentation for LangGraph, as well as some of our philosophies around organization and structure.
|
||||
|
||||
## Philosophy
|
||||
|
||||
LangGraph's documentation follows the [Diataxis framework](https://diataxis.fr).
|
||||
Under this framework, all documentation falls under one of four categories: [Tutorials](#tutorials),
|
||||
[How-to guides](#how-to-guides),
|
||||
[References](#references), and [Explanations (aka conceptual guides)](#conceptual-guide).
|
||||
|
||||
### Tutorials
|
||||
|
||||
Tutorials are lessons that take the reader through a practical activity. Their purpose is to help the user
|
||||
gain understanding of concepts and how they interact by showing one way to achieve some goal in a hands-on way.
|
||||
|
||||
They should **avoid** giving
|
||||
multiple permutations of ways to achieve that goal in-depth. Choice is burdensome. Instead, they should guide a new user through a recommended path to accomplishing a concrete goal. While the end result of a tutorial does not necessarily need to
|
||||
be completely production-ready, it should be useful and practically satisfy the goal that you clearly stated in the tutorial's introduction.
|
||||
|
||||
To quote the Diataxis website:
|
||||
|
||||
> A tutorial serves the user’s *acquisition* of skills and knowledge - their study. Its purpose is not to help the user get something done, but to help them learn.
|
||||
|
||||
In LangGraph, these are often higher level guides that show off end-to-end use cases.
|
||||
|
||||
Some examples include:
|
||||
|
||||
- [Build a Customer Support Bot](https://langchain-ai.github.io/langgraph/tutorials/customer-support/customer-support/)
|
||||
- [Build a SQL Agent](https://langchain-ai.github.io/langgraph/tutorials/sql/sql-agent/)
|
||||
|
||||
Here are some high-level tips on writing a good tutorial:
|
||||
|
||||
- Focus on guiding the user to get something done, but keep in mind the end-goal is more to impart principles than to create a perfect production system.
|
||||
- Be specific, not abstract and follow one path.
|
||||
- No need to go deeply into alternative approaches, but it’s ok to reference them, ideally with a link to an appropriate how-to guide.
|
||||
- Get "a point on the board" as soon as possible - something the user can run that outputs something.
|
||||
- You can iterate and expand afterwards.
|
||||
- Try to frequently checkpoint at given steps where the user can run code and see progress.
|
||||
- Focus on results, not technical explanation.
|
||||
- Crosslink heavily to appropriate conceptual/reference pages
|
||||
- The first time you mention a LangGraph concept, use its full name (e.g. "human-in-the-loop"), and link to its conceptual/other documentation page.
|
||||
- It's also helpful to add a prerequisite callout that links to any pages with necessary background information.
|
||||
- End with a recap/next steps section summarizing what the tutorial covered and future reading, such as related how-to guides.
|
||||
- Use phrases like "Next we can run X & Y. We will expect Z.". Then afterwards, use language like "Notice Z" that recalls our expectations and directs the reader's attention to the topic we are trying to teach.
|
||||
- Do not shy away from repetition.
|
||||
|
||||
### How-to guides
|
||||
|
||||
A how-to guide, as the name implies, demonstrates how to do something discrete and specific.
|
||||
It should assume that the user is already familiar with underlying concepts, and is trying to solve an immediate problem, but
|
||||
should still give some background or list the scenarios where the information contained within can be relevant.
|
||||
They can and should discuss alternatives if one approach may be better than another in certain cases.
|
||||
|
||||
To quote the Diataxis website:
|
||||
|
||||
> A how-to guide serves the work of the already-competent user, whom you can assume to know what they want to do, and to be able to follow your instructions correctly.
|
||||
|
||||
Some examples include:
|
||||
|
||||
- [How to add persistence to your graph](https://langchain-ai.github.io/langgraph/how-tos/persistence/)
|
||||
- [How to view and update past graph state](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/time-travel/)
|
||||
|
||||
Here are some high-level tips on writing a good how-to guide:
|
||||
|
||||
- Clearly explain what you are guiding the user through at the start
|
||||
- Assume higher intent than a tutorial and show what the user needs to do to get that task done
|
||||
- Assume familiarity of concepts, but explain why suggested actions are helpful
|
||||
- Crosslink heavily to conceptual/reference pages
|
||||
- Discuss alternatives and responses to real-world tradeoffs that may arise when solving a problem
|
||||
- Use lots of example code, ideally within complete code blocks that the reader can copy and run.
|
||||
- End with a recap/next steps section summarizing what the tutorial covered and future reading, such as other related how-to guides
|
||||
|
||||
### Conceptual guides
|
||||
|
||||
LangGraph's conceptual guides fall under the **Explanation** quadrant of Diataxis. They should cover LangChain terms and concepts
|
||||
in a more abstract way than how-to guides or tutorials, and should be geared towards curious users interested in
|
||||
gaining a deeper understanding of the framework. Try to avoid excessively large code examples. The goal here is to
|
||||
impart perspective to the user rather than to finish a practical project. These guides should cover **why** things work the way they do.
|
||||
|
||||
To quote the Diataxis website:
|
||||
|
||||
> The perspective of explanation is higher and wider than that of the other types. It does not take the user’s eye-level view, as in a how-to guide, or a close-up view of the machinery, like reference material. Its scope in each case is a topic - “an area of knowledge”, that somehow has to be bounded in a reasonable, meaningful way.
|
||||
|
||||
Some examples include:
|
||||
|
||||
- [What does it mean to be agentic?](https://langchain-ai.github.io/langgraph/concepts/high_level/)
|
||||
- [Tool calling](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#tool-calling)
|
||||
|
||||
Here are some high-level tips on writing a good conceptual guide:
|
||||
|
||||
- Explain design decisions. Why does concept X exist and why was it designed this way?
|
||||
- Use analogies and reference other concepts and alternatives
|
||||
- Avoid blending in too much reference content
|
||||
- You can and should reference content covered in other guides, but make sure to link to them
|
||||
|
||||
### References
|
||||
|
||||
References contain detailed, low-level information that describes exactly what functionality exists and how to use it.
|
||||
In LangGraph, this is mainly our API reference pages, which are populated from docstrings within code.
|
||||
References pages are generally not read end-to-end, but are consulted as necessary when a user needs to know
|
||||
how to use something specific.
|
||||
|
||||
To quote the Diataxis website:
|
||||
|
||||
> The only purpose of a reference guide is to describe, as succinctly as possible, and in an orderly way. Whereas the content of tutorials and how-to guides are led by needs of the user, reference material is led by the product it describes.
|
||||
|
||||
Many of the reference pages in LangChain are automatically generated from code,
|
||||
but here are some high-level tips on writing a good docstring:
|
||||
|
||||
- Be concise
|
||||
- Discuss special cases and deviations from a user's expectations
|
||||
- Go into detail on required inputs and outputs
|
||||
- Light details on when one might use the feature are fine, but in-depth details belong in other sections.
|
||||
|
||||
Each category serves a distinct purpose and requires a specific approach to writing and structuring the content.
|
||||
|
||||
## General guidelines
|
||||
|
||||
Here are some other guidelines you should think about when writing and organizing documentation.
|
||||
|
||||
We generally do not merge new tutorials from outside contributors without an actual need.
|
||||
We welcome updates as well as new integration docs, how-tos, and references.
|
||||
|
||||
### Avoid duplication
|
||||
|
||||
Multiple pages that cover the same material in depth are difficult to maintain and cause confusion. There should
|
||||
be only one (very rarely two), canonical pages for a given concept or feature. Instead, you should link to other guides.
|
||||
|
||||
### Link to other sections
|
||||
|
||||
Because sections of the docs do not exist in a vacuum, it is important to link to other sections as often as possible
|
||||
to allow a developer to learn more about an unfamiliar topic inline.
|
||||
|
||||
This includes linking to the API references as well as conceptual sections!
|
||||
|
||||
### Be concise
|
||||
|
||||
In general, take a less-is-more approach. If a section with a good explanation of a concept already exists, you should link to it rather than
|
||||
re-explain it, unless the concept you are documenting presents some new wrinkle.
|
||||
|
||||
Be concise, including in code samples.
|
||||
|
||||
### General style
|
||||
|
||||
- Use active voice and present tense whenever possible
|
||||
- Use examples and code snippets to illustrate concepts and usage
|
||||
- Use appropriate header levels (`#`, `##`, `###`, etc.) to organize the content hierarchically
|
||||
- Use fewer cells with more code to make copy/paste easier
|
||||
- Use bullet points and numbered lists to break down information into easily digestible chunks
|
||||
- Use tables (especially for **Reference** sections) and diagrams often to present information visually
|
||||
- Include the table of contents for longer documentation pages to help readers navigate the content, but hide it for shorter pages
|
||||
|
||||
## Setup
|
||||
|
||||
LangGraph documentation consists of two components:
|
||||
|
||||
1. Main Documentation: Hosted at [https://langchain-ai.github.io/langgraph/](https://langchain-ai.github.io/langgraph/),
|
||||
this comprehensive resource serves as the primary user-facing documentation.
|
||||
It covers a wide array of topics, including tutorials, use cases, integrations,
|
||||
and more, offering extensive guidance on building with LangGraph.
|
||||
The content for this documentation lives in the `/docs` directory of the monorepo.
|
||||
2. In-code Documentation: This is documentation of the codebase itself, which is also
|
||||
used to generate the externally facing [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/).
|
||||
The content for the API reference is autogenerated by scanning the docstrings in the codebase. For this reason we ask that developers document their code well.
|
||||
|
||||
We appreciate all contributions to the documentation, whether it be fixing a typo,
|
||||
adding a new tutorial or example and whether it be in the main documentation or the API Reference.
|
||||
|
||||
### 📜 Main Documentation
|
||||
|
||||
The content for the main documentation is located in the `/docs` directory of the monorepo.
|
||||
|
||||
The documentation is written using a combination of ipython notebooks (`.ipynb` files)
|
||||
and markdown (`.md` files). The notebooks are converted to markdown
|
||||
and then built using [MkDocs](https://www.mkdocs.org/).
|
||||
|
||||
Feel free to make contributions to the main documentation! 🥰
|
||||
|
||||
After modifying the documentation:
|
||||
|
||||
1. Run the linting and formatting commands (see below) to ensure that the documentation is well-formatted and free of errors.
|
||||
2. Optionally build the documentation locally to verify that the changes look good.
|
||||
3. Make a pull request with the changes.
|
||||
|
||||
### ⚒️ Linting and Building Documentation Locally
|
||||
|
||||
After writing up the documentation, you may want to lint and build the documentation
|
||||
locally to ensure that it looks good and is free of errors.
|
||||
|
||||
If you're unable to build it locally that's okay as well, as you will be able to
|
||||
see a preview of the documentation on the pull request page.
|
||||
|
||||
From the **monorepo root**, run the following command to install the dependencies:
|
||||
|
||||
<!-- TODO -->
|
||||
```bash
|
||||
poetry install --with docs --no-root
|
||||
```
|
||||
|
||||
#### Building
|
||||
|
||||
The code that builds the documentation is located in the `/docs` directory of the monorepo.
|
||||
|
||||
Before building the documentation, it is always a good idea to clean the build directory:
|
||||
|
||||
```bash
|
||||
make clean-docs
|
||||
```
|
||||
|
||||
You can build and preview the documentation as outlined below:
|
||||
|
||||
```bash
|
||||
make serve-docs
|
||||
```
|
||||
|
||||
#### Linting
|
||||
|
||||
To spell check the docs, run the following from the `docs` directory:
|
||||
|
||||
```bash
|
||||
codespell --skip="*.ambr,*.lock,*.ipynb,*.yaml,*.zlib,*.css.map,*.js.map" --ignore-words-list="infor,thead,stdio,nd,jupyter,lets,lite,uis,deque" .
|
||||
```
|
||||
|
||||
### ️In-code Documentation
|
||||
|
||||
The in-code documentation is autogenerated from docstrings.
|
||||
|
||||
For the API reference to be useful, the codebase must be well-documented. This means that all functions, classes, and methods should have a docstring that explains what they do, what the arguments are, and what the return value is. This is a good practice in general, but it is especially important for LangGraph because the API reference is the primary resource for developers to understand how to use the codebase.
|
||||
|
||||
We generally follow the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for docstrings.
|
||||
|
||||
Here is an example of a well-documented function:
|
||||
|
||||
```python
|
||||
|
||||
def my_function(arg1: int, arg2: str) -> float:
|
||||
"""This is a short description of the function. (It should be a single sentence.)
|
||||
|
||||
This is a longer description of the function. It should explain what
|
||||
the function does, what the arguments are, and what the return value is.
|
||||
It should wrap at 88 characters.
|
||||
|
||||
Examples:
|
||||
This is a section for examples of how to use the function.
|
||||
|
||||
```python
|
||||
my_function(1, "hello")
|
||||
\```
|
||||
|
||||
Args:
|
||||
arg1: This is a description of arg1. We do not need to specify the type since
|
||||
it is already specified in the function signature.
|
||||
arg2: This is a description of arg2.
|
||||
|
||||
Returns:
|
||||
This is a description of the return value.
|
||||
"""
|
||||
return 3.14
|
||||
```
|
||||
@@ -11,7 +11,7 @@
|
||||
[](https://pypi.org/project/langgraph/)
|
||||
[](https://pepy.tech/project/langgraph)
|
||||
[](https://github.com/langchain-ai/langgraph/issues)
|
||||
[](https://langchain-ai.github.io/langgraph/)
|
||||
[](https://docs.langchain.com/oss/python/langgraph/overview)
|
||||
|
||||
Trusted by companies shaping the future of agents – including Klarna, Replit, Elastic, and more – LangGraph is a low-level orchestration framework for building, managing, and deploying long-running, stateful agents.
|
||||
|
||||
@@ -23,47 +23,55 @@ Install LangGraph:
|
||||
pip install -U langgraph
|
||||
```
|
||||
|
||||
Then, create an agent [using prebuilt components](https://langchain-ai.github.io/langgraph/agents/agents/):
|
||||
Create a simple workflow:
|
||||
|
||||
```python
|
||||
# pip install -qU "langchain[anthropic]" to call the model
|
||||
from langgraph.graph import START, StateGraph
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
def get_weather(city: str) -> str:
|
||||
"""Get weather for a given city."""
|
||||
return f"It's always sunny in {city}!"
|
||||
class State(TypedDict):
|
||||
text: str
|
||||
|
||||
agent = create_react_agent(
|
||||
model="anthropic:claude-3-7-sonnet-latest",
|
||||
tools=[get_weather],
|
||||
prompt="You are a helpful assistant"
|
||||
)
|
||||
|
||||
# Run the agent
|
||||
agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]}
|
||||
)
|
||||
def node_a(state: State) -> dict:
|
||||
return {"text": state["text"] + "a"}
|
||||
|
||||
|
||||
def node_b(state: State) -> dict:
|
||||
return {"text": state["text"] + "b"}
|
||||
|
||||
|
||||
graph = StateGraph(State)
|
||||
graph.add_node("node_a", node_a)
|
||||
graph.add_node("node_b", node_b)
|
||||
graph.add_edge(START, "node_a")
|
||||
graph.add_edge("node_a", "node_b")
|
||||
|
||||
print(graph.compile().invoke({"text": ""}))
|
||||
# {'text': 'ab'}
|
||||
```
|
||||
|
||||
For more information, see the [Quickstart](https://langchain-ai.github.io/langgraph/agents/agents/). Or, to learn how to build an [agent workflow](https://langchain-ai.github.io/langgraph/concepts/low_level/) with a customizable architecture, long-term memory, and other complex task handling, see the [LangGraph basics tutorials](https://langchain-ai.github.io/langgraph/tutorials/get-started/1-build-basic-chatbot/).
|
||||
Get started with the [LangGraph Quickstart](https://docs.langchain.com/oss/python/langgraph/quickstart).
|
||||
|
||||
To quickly build agents with LangChain's `create_agent` (built on LangGraph), see the [LangChain Agents documentation](https://docs.langchain.com/oss/python/langchain/agents).
|
||||
|
||||
## Core benefits
|
||||
|
||||
LangGraph provides low-level supporting infrastructure for *any* long-running, stateful workflow or agent. LangGraph does not abstract prompts or architecture, and provides the following central benefits:
|
||||
|
||||
- [Durable execution](https://langchain-ai.github.io/langgraph/concepts/durable_execution/): Build agents that persist through failures and can run for extended periods, automatically resuming from exactly where they left off.
|
||||
- [Human-in-the-loop](https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/): Seamlessly incorporate human oversight by inspecting and modifying agent state at any point during execution.
|
||||
- [Comprehensive memory](https://langchain-ai.github.io/langgraph/concepts/memory/): Create truly stateful agents with both short-term working memory for ongoing reasoning and long-term persistent memory across sessions.
|
||||
- [Durable execution](https://docs.langchain.com/oss/python/langgraph/durable-execution): Build agents that persist through failures and can run for extended periods, automatically resuming from exactly where they left off.
|
||||
- [Human-in-the-loop](https://docs.langchain.com/oss/python/langgraph/interrupts): Seamlessly incorporate human oversight by inspecting and modifying agent state at any point during execution.
|
||||
- [Comprehensive memory](https://docs.langchain.com/oss/python/langgraph/memory): Create truly stateful agents with both short-term working memory for ongoing reasoning and long-term persistent memory across sessions.
|
||||
- [Debugging with LangSmith](http://www.langchain.com/langsmith): Gain deep visibility into complex agent behavior with visualization tools that trace execution paths, capture state transitions, and provide detailed runtime metrics.
|
||||
- [Production-ready deployment](https://langchain-ai.github.io/langgraph/concepts/deployment_options/): Deploy sophisticated agent systems confidently with scalable infrastructure designed to handle the unique challenges of stateful, long-running workflows.
|
||||
- [Production-ready deployment](https://docs.langchain.com/langsmith/app-development): Deploy sophisticated agent systems confidently with scalable infrastructure designed to handle the unique challenges of stateful, long-running workflows.
|
||||
|
||||
## LangGraph’s ecosystem
|
||||
|
||||
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
|
||||
|
||||
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
|
||||
- [LangSmith Deployment](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
|
||||
- [LangSmith Deployment](https://docs.langchain.com/langsmith/deployments) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://docs.langchain.com/oss/python/langgraph/studio).
|
||||
- [LangChain](https://docs.langchain.com/oss/python/langchain/overview) – Provides integrations and composable components to streamline LLM application development.
|
||||
|
||||
> [!NOTE]
|
||||
@@ -71,12 +79,11 @@ While LangGraph can be used standalone, it also integrates seamlessly with any L
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [Guides](https://langchain-ai.github.io/langgraph/guides/): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
|
||||
- [Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
|
||||
- [Examples](https://langchain-ai.github.io/langgraph/examples/): Guided examples on getting started with LangGraph.
|
||||
- [Guides](https://docs.langchain.com/oss/python/langgraph/guides): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
|
||||
- [Reference](https://reference.langchain.com/python/langgraph/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
|
||||
- [Examples](https://docs.langchain.com/oss/python/langgraph/agentic-rag): Guided examples on getting started with LangGraph.
|
||||
- [LangChain Forum](https://forum.langchain.com/): Connect with the community and share all of your technical questions, ideas, and feedback.
|
||||
- [LangChain Academy](https://academy.langchain.com/courses/intro-to-langgraph): Learn the basics of LangGraph in our free, structured course.
|
||||
- [Templates](https://langchain-ai.github.io/langgraph/concepts/template_applications/): Pre-built reference apps for common agentic workflows (e.g. ReAct agent, memory, retrieval etc.) that can be cloned and adapted.
|
||||
- [Case studies](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship AI applications at scale.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
@@ -143,11 +143,13 @@ class PostgresSaver(BasePostgresSaver):
|
||||
"""
|
||||
where, args = self._search_where(config, filter, before)
|
||||
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
params = list(args)
|
||||
if limit is not None:
|
||||
query += " LIMIT %s"
|
||||
params.append(int(limit))
|
||||
# if we change this to use .stream() we need to make sure to close the cursor
|
||||
with self._cursor() as cur:
|
||||
cur.execute(query, args)
|
||||
cur.execute(query, params)
|
||||
values = cur.fetchall()
|
||||
if not values:
|
||||
return
|
||||
|
||||
@@ -132,11 +132,13 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
"""
|
||||
where, args = self._search_where(config, filter, before)
|
||||
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
params = list(args)
|
||||
if limit is not None:
|
||||
query += " LIMIT %s"
|
||||
params.append(int(limit))
|
||||
# if we change this to use .stream() we need to make sure to close the cursor
|
||||
async with self._cursor() as cur:
|
||||
await cur.execute(query, args, binary=True)
|
||||
await cur.execute(query, params, binary=True)
|
||||
values = await cur.fetchall()
|
||||
if not values:
|
||||
return
|
||||
|
||||
@@ -272,10 +272,12 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
||||
"""
|
||||
where, args = self._search_where(config, filter, before)
|
||||
query = self.SELECT_SQL + where
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
params = list(args)
|
||||
if limit is not None:
|
||||
query += " LIMIT %s"
|
||||
params.append(int(limit))
|
||||
with self._cursor() as cur:
|
||||
cur.execute(self.SELECT_SQL + where, args, binary=True)
|
||||
cur.execute(query, params, binary=True)
|
||||
for value in cur:
|
||||
checkpoint: Checkpoint = {
|
||||
**value["checkpoint"],
|
||||
@@ -636,10 +638,12 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
"""
|
||||
where, args = self._search_where(config, filter, before)
|
||||
query = self.SELECT_SQL + where
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
params = list(args)
|
||||
if limit is not None:
|
||||
query += " LIMIT %s"
|
||||
params.append(int(limit))
|
||||
async with self._cursor() as cur:
|
||||
await cur.execute(self.SELECT_SQL + where, args, binary=True)
|
||||
await cur.execute(query, params, binary=True)
|
||||
async for value in cur:
|
||||
checkpoint: Checkpoint = {
|
||||
**value["checkpoint"],
|
||||
|
||||
@@ -266,6 +266,27 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
k: v(self) if v is not None and callable(v) else v
|
||||
for k, v in migration.params.items()
|
||||
}
|
||||
if "dims" in params:
|
||||
try:
|
||||
params["dims"] = int(params["dims"])
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
f"Invalid dims for vector index: {params['dims']}"
|
||||
) from e
|
||||
if "vector_type" in params:
|
||||
vt = str(params["vector_type"])
|
||||
if vt not in ("vector", "halfvec"):
|
||||
raise ValueError(
|
||||
f"Invalid vector_type for pgvector: {vt}"
|
||||
)
|
||||
params["vector_type"] = vt
|
||||
if "index_type" in params:
|
||||
it = str(params["index_type"])
|
||||
if it not in ("hnsw", "ivfflat"):
|
||||
raise ValueError(
|
||||
f"Invalid index_type for pgvector: {it}"
|
||||
)
|
||||
params["index_type"] = it
|
||||
sql = sql % params
|
||||
await cur.execute(sql)
|
||||
await cur.execute(
|
||||
|
||||
@@ -327,31 +327,36 @@ class BasePostgresStore(Generic[C]):
|
||||
embedding_request: tuple[str, Sequence[tuple[str, str, str, str]]] | None = None
|
||||
if inserts:
|
||||
values = []
|
||||
insertion_params = []
|
||||
insertion_params: list[Any] = []
|
||||
vector_values = []
|
||||
embedding_request_params = []
|
||||
# Handle TTL expiration
|
||||
|
||||
# First handle main store insertions
|
||||
for op in inserts:
|
||||
if op.ttl is not None:
|
||||
expires_at_str = f"NOW() + INTERVAL '{op.ttl * 60} seconds'"
|
||||
ttl_minutes = op.ttl
|
||||
else:
|
||||
expires_at_str = "NULL"
|
||||
ttl_minutes = None
|
||||
|
||||
values.append(
|
||||
f"(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, {expires_at_str}, %s)"
|
||||
)
|
||||
insertion_params.extend(
|
||||
[
|
||||
(
|
||||
_namespace_to_text(op.namespace),
|
||||
op.key,
|
||||
Jsonb(cast(dict, op.value)),
|
||||
ttl_minutes,
|
||||
]
|
||||
)
|
||||
)
|
||||
if op.ttl is not None:
|
||||
values.append(
|
||||
"(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NOW() + %s::interval, %s)"
|
||||
)
|
||||
ttl_minutes = float(op.ttl)
|
||||
insertion_params.extend(
|
||||
(
|
||||
f"{ttl_minutes * 60} seconds",
|
||||
ttl_minutes,
|
||||
)
|
||||
)
|
||||
else:
|
||||
values.append(
|
||||
"(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, %s)"
|
||||
)
|
||||
insertion_params.append(None)
|
||||
|
||||
# Then handle embeddings if configured
|
||||
if self.index_config:
|
||||
@@ -465,6 +470,10 @@ class BasePostgresStore(Generic[C]):
|
||||
cast(dict, self.index_config)["dims"],
|
||||
)
|
||||
else:
|
||||
if vector_type not in ("vector", "halfvec"):
|
||||
raise ValueError(
|
||||
f"Invalid vector_type for pgvector: {vector_type}"
|
||||
)
|
||||
score_operator = score_operator % ("%s", vector_type)
|
||||
|
||||
vectors_per_doc_estimate = cast(dict, self.index_config)[
|
||||
@@ -1122,6 +1131,27 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
k: v(self) if v is not None and callable(v) else v
|
||||
for k, v in migration.params.items()
|
||||
}
|
||||
if "dims" in params:
|
||||
try:
|
||||
params["dims"] = int(params["dims"])
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
f"Invalid dims for vector index: {params['dims']}"
|
||||
) from e
|
||||
if "vector_type" in params:
|
||||
vt = str(params["vector_type"])
|
||||
if vt not in ("vector", "halfvec"):
|
||||
raise ValueError(
|
||||
f"Invalid vector_type for pgvector: {vt}"
|
||||
)
|
||||
params["vector_type"] = vt
|
||||
if "index_type" in params:
|
||||
it = str(params["index_type"])
|
||||
if it not in ("hnsw", "ivfflat"):
|
||||
raise ValueError(
|
||||
f"Invalid index_type for pgvector: {it}"
|
||||
)
|
||||
params["index_type"] = it
|
||||
sql = sql % params
|
||||
cur.execute(sql)
|
||||
cur.execute("INSERT INTO vector_migrations (v) VALUES (%s)", (v,))
|
||||
@@ -1175,15 +1205,44 @@ def _get_vector_type_ops(store: BasePostgresStore) -> str:
|
||||
|
||||
|
||||
def _get_index_params(store: Any) -> tuple[str, dict[str, Any]]:
|
||||
"""Get the index type and configuration based on config."""
|
||||
"""Get a sanitized index type and configuration based on config.
|
||||
|
||||
Only allow known-safe kinds and integer parameters to avoid SQL injection
|
||||
when constructing DDL strings for index creation.
|
||||
"""
|
||||
if not store.index_config:
|
||||
return "hnsw", {}
|
||||
|
||||
config = cast(PostgresIndexConfig, store.index_config)
|
||||
index_config = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
|
||||
kind = index_config.pop("kind", "hnsw")
|
||||
index_config.pop("vector_type", None)
|
||||
return kind, index_config
|
||||
raw = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
|
||||
|
||||
kind = str(raw.pop("kind", "hnsw"))
|
||||
if kind not in ("hnsw", "ivfflat", "flat"):
|
||||
raise ValueError(
|
||||
f"Invalid index kind for pgvector: {kind}. Expected 'hnsw', 'ivfflat', or 'flat'."
|
||||
)
|
||||
|
||||
raw.pop("vector_type", None)
|
||||
|
||||
if kind == "hnsw":
|
||||
allowed_keys = {"m", "ef_construction"}
|
||||
else: # ivfflat/flat
|
||||
allowed_keys = {"lists", "nlist"}
|
||||
|
||||
sanitized: dict[str, int] = {}
|
||||
for k, v in list(raw.items()):
|
||||
if k not in allowed_keys:
|
||||
continue
|
||||
key = "lists" if k == "nlist" else k
|
||||
try:
|
||||
ivalue = int(v) # type: ignore[call-overload]
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid index parameter value for {k}: {v}") from e
|
||||
if ivalue <= 0:
|
||||
continue
|
||||
sanitized[key] = ivalue
|
||||
|
||||
return kind, sanitized
|
||||
|
||||
|
||||
def _namespace_to_text(
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "3.0.1"
|
||||
version = "3.0.2"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Generated
+564
-416
File diff suppressed because it is too large
Load Diff
@@ -329,8 +329,9 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
||||
FROM checkpoints
|
||||
{where}
|
||||
ORDER BY checkpoint_id DESC"""
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
if limit is not None:
|
||||
query += " LIMIT ?"
|
||||
param_values = (*param_values, limit)
|
||||
with self.cursor(transaction=False) as cur, closing(self.conn.cursor()) as wcur:
|
||||
cur.execute(query, param_values)
|
||||
for (
|
||||
|
||||
@@ -425,8 +425,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
||||
FROM checkpoints
|
||||
{where}
|
||||
ORDER BY checkpoint_id DESC"""
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
if limit is not None:
|
||||
query += " LIMIT ?"
|
||||
params = (*params, limit)
|
||||
async with (
|
||||
self.lock,
|
||||
self.conn.execute(query, params) as cur,
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.checkpoint.base import get_checkpoint_id
|
||||
|
||||
_FILTER_PATTERN = re.compile(r"^[a-zA-Z0-9_.-]+$")
|
||||
|
||||
|
||||
def _validate_filter_key(key: str) -> None:
|
||||
"""Validate that a filter key is safe for use in SQL queries.
|
||||
|
||||
Args:
|
||||
key: The filter key to validate
|
||||
|
||||
Raises:
|
||||
ValueError: If the key contains invalid characters that could enable SQL injection
|
||||
"""
|
||||
# Allow alphanumeric characters, underscores, dots, and hyphens
|
||||
# This covers typical JSON property names while preventing SQL injection
|
||||
if not _FILTER_PATTERN.match(key):
|
||||
raise ValueError(
|
||||
f"Invalid filter key: '{key}'. Filter keys must contain only alphanumeric characters, underscores, dots, and hyphens."
|
||||
)
|
||||
|
||||
|
||||
def _metadata_predicate(
|
||||
metadata_filter: dict[str, Any],
|
||||
@@ -43,6 +63,7 @@ def _metadata_predicate(
|
||||
|
||||
# process metadata query
|
||||
for query_key, query_value in metadata_filter.items():
|
||||
_validate_filter_key(query_key)
|
||||
operator, param_value = _where_value(query_value)
|
||||
predicates.append(
|
||||
f"json_extract(CAST(metadata AS TEXT), '$.{query_key}') {operator}"
|
||||
|
||||
@@ -107,6 +107,9 @@ def _decode_ns_text(namespace: str) -> tuple[str, ...]:
|
||||
return tuple(namespace.split("."))
|
||||
|
||||
|
||||
_FILTER_PATTERN = re.compile(r"^[a-zA-Z0-9_.-]+$")
|
||||
|
||||
|
||||
def _validate_filter_key(key: str) -> None:
|
||||
"""Validate that a filter key is safe for use in SQL queries.
|
||||
|
||||
@@ -118,7 +121,7 @@ def _validate_filter_key(key: str) -> None:
|
||||
"""
|
||||
# Allow alphanumeric characters, underscores, dots, and hyphens
|
||||
# This covers typical JSON property names while preventing SQL injection
|
||||
if not re.match(r"^[a-zA-Z0-9_.-]+$", key):
|
||||
if not _FILTER_PATTERN.match(key):
|
||||
raise ValueError(
|
||||
f"Invalid filter key: '{key}'. Filter keys must contain only alphanumeric characters, underscores, dots, and hyphens."
|
||||
)
|
||||
@@ -404,12 +407,9 @@ class BaseSqliteStore:
|
||||
# SQLite json_extract returns unquoted string values
|
||||
if isinstance(value, str):
|
||||
filter_conditions.append(
|
||||
"json_extract(value, '$."
|
||||
+ key
|
||||
+ "') = '"
|
||||
+ value.replace("'", "''")
|
||||
+ "'"
|
||||
"json_extract(value, '$." + key + "') = ?"
|
||||
)
|
||||
filter_params.append(value)
|
||||
elif value is None:
|
||||
filter_conditions.append(
|
||||
"json_extract(value, '$." + key + "') IS NULL"
|
||||
@@ -423,9 +423,11 @@ class BaseSqliteStore:
|
||||
+ ("1" if value else "0")
|
||||
)
|
||||
elif isinstance(value, (int, float)):
|
||||
# Use parameterized query to handle special floats and large integers
|
||||
filter_conditions.append(
|
||||
"json_extract(value, '$." + key + "') = " + str(value)
|
||||
"json_extract(value, '$." + key + "') = ?"
|
||||
)
|
||||
filter_params.append(float(value))
|
||||
else:
|
||||
# Complex objects (list, dict, …) – compare JSON text
|
||||
filter_conditions.append(
|
||||
@@ -636,85 +638,66 @@ class BaseSqliteStore:
|
||||
# We need to properly format values for SQLite JSON extraction comparison
|
||||
if op == "$eq":
|
||||
if isinstance(value, str):
|
||||
# Direct string comparison with proper quoting for unquoted json_extract result
|
||||
return (
|
||||
f"json_extract(value, '$.{key}') = '"
|
||||
+ value.replace("'", "''")
|
||||
+ "'",
|
||||
[],
|
||||
)
|
||||
return f"json_extract(value, '$.{key}') = ?", [value]
|
||||
elif value is None:
|
||||
return f"json_extract(value, '$.{key}') IS NULL", []
|
||||
elif isinstance(value, bool):
|
||||
# SQLite JSON stores booleans as integers
|
||||
return f"json_extract(value, '$.{key}') = {1 if value else 0}", []
|
||||
elif isinstance(value, (int, float)):
|
||||
return f"json_extract(value, '$.{key}') = {value}", []
|
||||
# Convert to float to handle inf, -inf, nan, and very large integers
|
||||
# SQLite REAL can handle these cases better than INTEGER
|
||||
return f"json_extract(value, '$.{key}') = ?", [float(value)]
|
||||
else:
|
||||
return f"json_extract(value, '$.{key}') = ?", [orjson.dumps(value)]
|
||||
elif op == "$gt":
|
||||
# For numeric values, SQLite needs to compare as numbers, not strings
|
||||
if isinstance(value, (int, float)):
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) > {value}", []
|
||||
# Convert to float to handle special values and very large integers
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) > ?", [
|
||||
float(value)
|
||||
]
|
||||
elif isinstance(value, str):
|
||||
return (
|
||||
f"json_extract(value, '$.{key}') > '"
|
||||
+ value.replace("'", "''")
|
||||
+ "'",
|
||||
[],
|
||||
)
|
||||
return f"json_extract(value, '$.{key}') > ?", [value]
|
||||
else:
|
||||
return f"json_extract(value, '$.{key}') > ?", [orjson.dumps(value)]
|
||||
elif op == "$gte":
|
||||
if isinstance(value, (int, float)):
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) >= {value}", []
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) >= ?", [
|
||||
float(value)
|
||||
]
|
||||
elif isinstance(value, str):
|
||||
return (
|
||||
f"json_extract(value, '$.{key}') >= '"
|
||||
+ value.replace("'", "''")
|
||||
+ "'",
|
||||
[],
|
||||
)
|
||||
return f"json_extract(value, '$.{key}') >= ?", [value]
|
||||
else:
|
||||
return f"json_extract(value, '$.{key}') >= ?", [orjson.dumps(value)]
|
||||
elif op == "$lt":
|
||||
if isinstance(value, (int, float)):
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) < {value}", []
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) < ?", [
|
||||
float(value)
|
||||
]
|
||||
elif isinstance(value, str):
|
||||
return (
|
||||
f"json_extract(value, '$.{key}') < '"
|
||||
+ value.replace("'", "''")
|
||||
+ "'",
|
||||
[],
|
||||
)
|
||||
return f"json_extract(value, '$.{key}') < ?", [value]
|
||||
else:
|
||||
return f"json_extract(value, '$.{key}') < ?", [orjson.dumps(value)]
|
||||
elif op == "$lte":
|
||||
if isinstance(value, (int, float)):
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) <= {value}", []
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) <= ?", [
|
||||
float(value)
|
||||
]
|
||||
elif isinstance(value, str):
|
||||
return (
|
||||
f"json_extract(value, '$.{key}') <= '"
|
||||
+ value.replace("'", "''")
|
||||
+ "'",
|
||||
[],
|
||||
)
|
||||
return f"json_extract(value, '$.{key}') <= ?", [value]
|
||||
else:
|
||||
return f"json_extract(value, '$.{key}') <= ?", [orjson.dumps(value)]
|
||||
elif op == "$ne":
|
||||
if isinstance(value, str):
|
||||
return (
|
||||
f"json_extract(value, '$.{key}') != '"
|
||||
+ value.replace("'", "''")
|
||||
+ "'",
|
||||
[],
|
||||
)
|
||||
return f"json_extract(value, '$.{key}') != ?", [value]
|
||||
elif value is None:
|
||||
return f"json_extract(value, '$.{key}') IS NOT NULL", []
|
||||
elif isinstance(value, bool):
|
||||
return f"json_extract(value, '$.{key}') != {1 if value else 0}", []
|
||||
elif isinstance(value, (int, float)):
|
||||
return f"json_extract(value, '$.{key}') != {value}", []
|
||||
# Convert to float for consistency
|
||||
return f"json_extract(value, '$.{key}') != ?", [float(value)]
|
||||
else:
|
||||
return f"json_extract(value, '$.{key}') != ?", [orjson.dumps(value)]
|
||||
else:
|
||||
@@ -792,8 +775,9 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
||||
self,
|
||||
conn: sqlite3.Connection,
|
||||
*,
|
||||
deserializer: Callable[[bytes | str | orjson.Fragment], dict[str, Any]]
|
||||
| None = None,
|
||||
deserializer: (
|
||||
Callable[[bytes | str | orjson.Fragment], dict[str, Any]] | None
|
||||
) = None,
|
||||
index: SqliteIndexConfig | None = None,
|
||||
ttl: TTLConfig | None = None,
|
||||
):
|
||||
@@ -874,85 +858,66 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
||||
# We need to properly format values for SQLite JSON extraction comparison
|
||||
if op == "$eq":
|
||||
if isinstance(value, str):
|
||||
# Direct string comparison with proper quoting for unquoted json_extract result
|
||||
return (
|
||||
f"json_extract(value, '$.{key}') = '"
|
||||
+ value.replace("'", "''")
|
||||
+ "'",
|
||||
[],
|
||||
)
|
||||
return f"json_extract(value, '$.{key}') = ?", [value]
|
||||
elif value is None:
|
||||
return f"json_extract(value, '$.{key}') IS NULL", []
|
||||
elif isinstance(value, bool):
|
||||
# SQLite JSON stores booleans as integers
|
||||
return f"json_extract(value, '$.{key}') = {1 if value else 0}", []
|
||||
elif isinstance(value, (int, float)):
|
||||
return f"json_extract(value, '$.{key}') = {value}", []
|
||||
# Convert to float to handle inf, -inf, nan, and very large integers
|
||||
# SQLite REAL can handle these cases better than INTEGER
|
||||
return f"json_extract(value, '$.{key}') = ?", [float(value)]
|
||||
else:
|
||||
return f"json_extract(value, '$.{key}') = ?", [orjson.dumps(value)]
|
||||
elif op == "$gt":
|
||||
# For numeric values, SQLite needs to compare as numbers, not strings
|
||||
if isinstance(value, (int, float)):
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) > {value}", []
|
||||
# Convert to float to handle special values and very large integers
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) > ?", [
|
||||
float(value)
|
||||
]
|
||||
elif isinstance(value, str):
|
||||
return (
|
||||
f"json_extract(value, '$.{key}') > '"
|
||||
+ value.replace("'", "''")
|
||||
+ "'",
|
||||
[],
|
||||
)
|
||||
return f"json_extract(value, '$.{key}') > ?", [value]
|
||||
else:
|
||||
return f"json_extract(value, '$.{key}') > ?", [orjson.dumps(value)]
|
||||
elif op == "$gte":
|
||||
if isinstance(value, (int, float)):
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) >= {value}", []
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) >= ?", [
|
||||
float(value)
|
||||
]
|
||||
elif isinstance(value, str):
|
||||
return (
|
||||
f"json_extract(value, '$.{key}') >= '"
|
||||
+ value.replace("'", "''")
|
||||
+ "'",
|
||||
[],
|
||||
)
|
||||
return f"json_extract(value, '$.{key}') >= ?", [value]
|
||||
else:
|
||||
return f"json_extract(value, '$.{key}') >= ?", [orjson.dumps(value)]
|
||||
elif op == "$lt":
|
||||
if isinstance(value, (int, float)):
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) < {value}", []
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) < ?", [
|
||||
float(value)
|
||||
]
|
||||
elif isinstance(value, str):
|
||||
return (
|
||||
f"json_extract(value, '$.{key}') < '"
|
||||
+ value.replace("'", "''")
|
||||
+ "'",
|
||||
[],
|
||||
)
|
||||
return f"json_extract(value, '$.{key}') < ?", [value]
|
||||
else:
|
||||
return f"json_extract(value, '$.{key}') < ?", [orjson.dumps(value)]
|
||||
elif op == "$lte":
|
||||
if isinstance(value, (int, float)):
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) <= {value}", []
|
||||
return f"CAST(json_extract(value, '$.{key}') AS REAL) <= ?", [
|
||||
float(value)
|
||||
]
|
||||
elif isinstance(value, str):
|
||||
return (
|
||||
f"json_extract(value, '$.{key}') <= '"
|
||||
+ value.replace("'", "''")
|
||||
+ "'",
|
||||
[],
|
||||
)
|
||||
return f"json_extract(value, '$.{key}') <= ?", [value]
|
||||
else:
|
||||
return f"json_extract(value, '$.{key}') <= ?", [orjson.dumps(value)]
|
||||
elif op == "$ne":
|
||||
if isinstance(value, str):
|
||||
return (
|
||||
f"json_extract(value, '$.{key}') != '"
|
||||
+ value.replace("'", "''")
|
||||
+ "'",
|
||||
[],
|
||||
)
|
||||
return f"json_extract(value, '$.{key}') != ?", [value]
|
||||
elif value is None:
|
||||
return f"json_extract(value, '$.{key}') IS NOT NULL", []
|
||||
elif isinstance(value, bool):
|
||||
return f"json_extract(value, '$.{key}') != {1 if value else 0}", []
|
||||
elif isinstance(value, (int, float)):
|
||||
return f"json_extract(value, '$.{key}') != {value}", []
|
||||
# Convert to float for consistency
|
||||
return f"json_extract(value, '$.{key}') != ?", [float(value)]
|
||||
else:
|
||||
return f"json_extract(value, '$.{key}') != ?", [orjson.dumps(value)]
|
||||
else:
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "3.0.0"
|
||||
version = "3.0.1"
|
||||
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -113,4 +113,78 @@ class TestAsyncSqliteSaver:
|
||||
search_results_5[1].config["configurable"]["checkpoint_ns"],
|
||||
} == {"", "inner"}
|
||||
|
||||
# TODO: test before and limit params
|
||||
# Test limit param
|
||||
search_results_6 = [
|
||||
c
|
||||
async for c in saver.alist(
|
||||
{"configurable": {"thread_id": "thread-2"}}, limit=1
|
||||
)
|
||||
]
|
||||
assert len(search_results_6) == 1
|
||||
assert search_results_6[0].config["configurable"]["thread_id"] == "thread-2"
|
||||
|
||||
# Test before param
|
||||
search_results_7 = [
|
||||
c async for c in saver.alist(None, before=search_results_5[1].config)
|
||||
]
|
||||
assert len(search_results_7) == 1
|
||||
assert search_results_7[0].config["configurable"]["thread_id"] == "thread-1"
|
||||
|
||||
async def test_limit_parameter_sql_injection_prevention(self) -> None:
|
||||
"""Test that the limit parameter properly uses parameterized queries to prevent SQL injection."""
|
||||
async with AsyncSqliteSaver.from_conn_string(":memory:") as saver:
|
||||
# Setup: Create multiple checkpoints
|
||||
for i in range(5):
|
||||
config: RunnableConfig = {
|
||||
"configurable": {
|
||||
"thread_id": f"thread-{i}",
|
||||
"checkpoint_ns": "",
|
||||
}
|
||||
}
|
||||
checkpoint = empty_checkpoint()
|
||||
metadata: CheckpointMetadata = {"index": i}
|
||||
await saver.aput(config, checkpoint, metadata, {})
|
||||
|
||||
# Test that limit works correctly with valid integer
|
||||
results = [c async for c in saver.alist(None, limit=2)]
|
||||
assert len(results) == 2
|
||||
|
||||
# Test that limit=0 returns no results
|
||||
results = [c async for c in saver.alist(None, limit=0)]
|
||||
assert len(results) == 0
|
||||
|
||||
# Test that limit=None returns all results
|
||||
results = [c async for c in saver.alist(None, limit=None)]
|
||||
assert len(results) == 5
|
||||
|
||||
# Test explicit SQL injection attempt via limit parameter
|
||||
# Even if type checking is bypassed and a malicious string is passed,
|
||||
# the parameterized query will treat it as a value, not SQL code
|
||||
# This would cause an error (can't convert string to int for LIMIT),
|
||||
# which is the correct secure behavior
|
||||
malicious_limits = [
|
||||
"1; DROP TABLE checkpoints; --",
|
||||
"1 OR 1=1",
|
||||
"999999 UNION SELECT * FROM checkpoints",
|
||||
]
|
||||
|
||||
for malicious_limit in malicious_limits:
|
||||
# The parameterized query should safely reject non-integer limits
|
||||
# or convert them in a way that prevents SQL injection
|
||||
try:
|
||||
# Bypass type checking by casting
|
||||
results = [
|
||||
c
|
||||
async for c in saver.alist(None, limit=malicious_limit) # type: ignore
|
||||
]
|
||||
# If it doesn't raise an error, it should at least not execute the injection
|
||||
# SQLite's parameter binding will try to convert the string to an integer
|
||||
# which will either fail or treat it as 0
|
||||
except Exception:
|
||||
# Expected: SQLite should reject invalid limit values
|
||||
pass
|
||||
|
||||
# Verify the checkpoints table still exists and has all data
|
||||
# (would have been dropped if injection succeeded)
|
||||
results = [c async for c in saver.alist(None, limit=None)]
|
||||
assert len(results) == 5
|
||||
|
||||
@@ -182,3 +182,128 @@ class TestSqliteSaver:
|
||||
with pytest.raises(NotImplementedError, match="AsyncSqliteSaver"):
|
||||
async for _ in saver.alist(self.config_1):
|
||||
pass
|
||||
|
||||
def test_metadata_predicate_sql_injection_prevention(self) -> None:
|
||||
"""Test that _metadata_predicate rejects malicious filter keys."""
|
||||
# Test various SQL injection payloads
|
||||
malicious_keys = [
|
||||
"x') OR '1'='1", # Boolean-based injection
|
||||
"x') OR 1=1 --", # Comment-based injection
|
||||
"x') UNION SELECT 1,2,3,4,5,6,7 --", # UNION-based injection
|
||||
"access') = 'public' OR '1'='1' OR json_extract(value, '$.", # Complex injection
|
||||
"'; DROP TABLE checkpoints; --", # Destructive injection
|
||||
]
|
||||
|
||||
for malicious_key in malicious_keys:
|
||||
with pytest.raises(ValueError, match="Invalid filter key"):
|
||||
_metadata_predicate({malicious_key: "dummy"})
|
||||
|
||||
def test_checkpoint_search_sql_injection_prevention(self) -> None:
|
||||
"""Test that SQL injection via malicious filter keys is prevented in checkpoint search."""
|
||||
with SqliteSaver.from_conn_string(":memory:") as saver:
|
||||
# Setup: Create checkpoints with different metadata
|
||||
config_public: RunnableConfig = {
|
||||
"configurable": {
|
||||
"thread_id": "thread-public",
|
||||
"checkpoint_ns": "",
|
||||
}
|
||||
}
|
||||
config_private: RunnableConfig = {
|
||||
"configurable": {
|
||||
"thread_id": "thread-private",
|
||||
"checkpoint_ns": "",
|
||||
}
|
||||
}
|
||||
|
||||
checkpoint_public = empty_checkpoint()
|
||||
checkpoint_private = empty_checkpoint()
|
||||
|
||||
metadata_public: CheckpointMetadata = {
|
||||
"access": "public",
|
||||
"data": "public information",
|
||||
}
|
||||
metadata_private: CheckpointMetadata = {
|
||||
"access": "private",
|
||||
"data": "secret information",
|
||||
"password": "secret123",
|
||||
}
|
||||
|
||||
saver.put(config_public, checkpoint_public, metadata_public, {})
|
||||
saver.put(config_private, checkpoint_private, metadata_private, {})
|
||||
|
||||
# Normal query - should return only public checkpoint
|
||||
normal_results = list(saver.list(None, filter={"access": "public"}))
|
||||
assert len(normal_results) == 1
|
||||
assert normal_results[0].metadata["access"] == "public"
|
||||
|
||||
# SQL injection attempt should raise ValueError
|
||||
malicious_key = (
|
||||
"access') = 'public' OR '1'='1' OR json_extract(metadata, '$."
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid filter key"):
|
||||
list(saver.list(None, filter={malicious_key: "dummy"}))
|
||||
|
||||
def test_limit_parameter_sql_injection_prevention(self) -> None:
|
||||
"""Test that the limit parameter properly uses parameterized queries to prevent SQL injection."""
|
||||
with SqliteSaver.from_conn_string(":memory:") as saver:
|
||||
# Setup: Create multiple checkpoints
|
||||
for i in range(5):
|
||||
config: RunnableConfig = {
|
||||
"configurable": {
|
||||
"thread_id": f"thread-{i}",
|
||||
"checkpoint_ns": "",
|
||||
}
|
||||
}
|
||||
checkpoint = empty_checkpoint()
|
||||
metadata: CheckpointMetadata = {"index": i}
|
||||
saver.put(config, checkpoint, metadata, {})
|
||||
|
||||
# Test that limit works correctly with valid integer
|
||||
results = list(saver.list(None, limit=2))
|
||||
assert len(results) == 2
|
||||
|
||||
# Test that limit=0 returns no results
|
||||
results = list(saver.list(None, limit=0))
|
||||
assert len(results) == 0
|
||||
|
||||
# Test that limit=None returns all results
|
||||
results = list(saver.list(None, limit=None))
|
||||
assert len(results) == 5
|
||||
|
||||
def test_metadata_filter_keys_with_hyphens_and_digits(self) -> None:
|
||||
"""Metadata keys with hyphens and digit-start should be filterable.
|
||||
|
||||
This exposes incorrect JSON path handling (unquoted segments) by asserting
|
||||
that such filters successfully match saved checkpoints.
|
||||
"""
|
||||
with SqliteSaver.from_conn_string(":memory:") as saver:
|
||||
config: RunnableConfig = {
|
||||
"configurable": {
|
||||
"thread_id": "thread-hyphen-digit",
|
||||
"checkpoint_ns": "",
|
||||
}
|
||||
}
|
||||
checkpoint = empty_checkpoint()
|
||||
metadata: CheckpointMetadata = {
|
||||
"access-level": "public",
|
||||
"user": {"access-level": "nested", "123abc": "ok2"},
|
||||
"123abc": "ok",
|
||||
}
|
||||
saver.put(config, checkpoint, metadata, {})
|
||||
|
||||
# Top-level hyphenated key
|
||||
results = list(saver.list(None, filter={"access-level": "public"}))
|
||||
assert len(results) == 1
|
||||
|
||||
# Nested hyphenated key via dotted path
|
||||
results = list(saver.list(None, filter={"user.access-level": "nested"}))
|
||||
assert len(results) == 1
|
||||
|
||||
# Top-level digit-starting key
|
||||
results = list(saver.list(None, filter={"123abc": "ok"}))
|
||||
assert len(results) == 1
|
||||
|
||||
# Nested digit-starting key via dotted path
|
||||
results = list(saver.list(None, filter={"user.123abc": "ok2"}))
|
||||
assert len(results) == 1
|
||||
|
||||
@@ -1069,6 +1069,141 @@ def test_sql_injection_vulnerability(store: SqliteStore) -> None:
|
||||
store.search(("docs",), filter={malicious_key: "dummy"})
|
||||
|
||||
|
||||
def test_sql_injection_filter_values(store: SqliteStore) -> None:
|
||||
"""Test that SQL injection via malicious filter values is properly escaped."""
|
||||
# Setup: Create documents with different access levels
|
||||
store.put(("docs",), "doc1", {"access": "public", "title": "Public Document"})
|
||||
store.put(("docs",), "doc2", {"access": "private", "title": "Private Document"})
|
||||
store.put(("docs",), "doc3", {"access": "secret", "title": "Secret Document"})
|
||||
|
||||
# Test 1: Basic SQL injection attempt with single quote
|
||||
malicious_value = "public' OR '1'='1"
|
||||
results = store.search(("docs",), filter={"access": malicious_value})
|
||||
# Should return 0 results because the malicious value is escaped and won't match anything
|
||||
assert len(results) == 0, "SQL injection via string value should be blocked"
|
||||
|
||||
# Test 2: SQL injection with comment
|
||||
malicious_value = "public'; --"
|
||||
results = store.search(("docs",), filter={"access": malicious_value})
|
||||
assert len(results) == 0, "SQL comment injection should be blocked"
|
||||
|
||||
# Test 3: UNION injection attempt
|
||||
malicious_value = "public' UNION SELECT * FROM store --"
|
||||
results = store.search(("docs",), filter={"access": malicious_value})
|
||||
assert len(results) == 0, "UNION injection should be blocked"
|
||||
|
||||
# Test 4: Parameterized queries handle strings with null bytes and SQL injection attempts safely
|
||||
malicious_value = "public\x00' OR '1'='1"
|
||||
results = store.search(("docs",), filter={"access": malicious_value})
|
||||
assert len(results) == 0, (
|
||||
"Parameterized queries treat injection attempts as literal strings"
|
||||
)
|
||||
|
||||
# Test 5: Multiple single quotes
|
||||
malicious_value = "''''"
|
||||
results = store.search(("docs",), filter={"access": malicious_value})
|
||||
assert len(results) == 0, "Multiple quotes should be handled safely"
|
||||
|
||||
# Test 6: Legitimate value with single quote should work
|
||||
store.put(("docs",), "doc4", {"title": "O'Brien's Document", "access": "public"})
|
||||
results = store.search(("docs",), filter={"title": "O'Brien's Document"})
|
||||
assert len(results) == 1, "Legitimate single quotes should work"
|
||||
assert results[0].value["title"] == "O'Brien's Document"
|
||||
|
||||
# Test 7: Unicode characters with injection attempt
|
||||
malicious_value = "public' OR 'א'='א"
|
||||
results = store.search(("docs",), filter={"access": malicious_value})
|
||||
assert len(results) == 0, "Unicode-based injection should be blocked"
|
||||
|
||||
|
||||
def test_numeric_filter_safety(store: SqliteStore) -> None:
|
||||
"""Test that numeric filter values are handled safely."""
|
||||
# Setup: Create documents with numeric fields
|
||||
store.put(("items",), "item1", {"price": 10, "quantity": 5})
|
||||
store.put(("items",), "item2", {"price": 20, "quantity": 3})
|
||||
store.put(("items",), "item3", {"price": 30, "quantity": 1})
|
||||
|
||||
# Test 1: Normal numeric comparison
|
||||
results = store.search(("items",), filter={"price": {"$gt": 15}})
|
||||
assert len(results) == 2
|
||||
assert all(r.value["price"] > 15 for r in results)
|
||||
|
||||
# Test 2: Special float values (infinity)
|
||||
results = store.search(("items",), filter={"price": {"$lt": float("inf")}})
|
||||
assert len(results) == 3, "All finite values should be less than infinity"
|
||||
|
||||
# Test 3: Special float values (negative infinity)
|
||||
results = store.search(("items",), filter={"price": {"$gt": float("-inf")}})
|
||||
assert len(results) == 3, (
|
||||
"All finite values should be greater than negative infinity"
|
||||
)
|
||||
|
||||
# Test 4: NaN handling - NaN comparisons should not cause errors
|
||||
try:
|
||||
results = store.search(("items",), filter={"price": {"$eq": float("nan")}})
|
||||
# NaN never equals anything, including itself, so should return 0 results
|
||||
assert len(results) == 0
|
||||
except Exception as e:
|
||||
pytest.fail(f"NaN handling should not raise exception: {e}")
|
||||
|
||||
# Test 5: Very large numbers
|
||||
results = store.search(("items",), filter={"price": {"$lt": 10**100}})
|
||||
assert len(results) == 3, "Very large numbers should be handled safely"
|
||||
|
||||
# Test 6: Negative numbers
|
||||
store.put(("items",), "item4", {"price": -10, "quantity": 0})
|
||||
results = store.search(("items",), filter={"price": {"$lt": 0}})
|
||||
assert len(results) == 1
|
||||
assert results[0].key == "item4"
|
||||
|
||||
|
||||
def test_boolean_filter_safety(store: SqliteStore) -> None:
|
||||
"""Test that boolean filter values are handled safely."""
|
||||
store.put(("flags",), "flag1", {"active": True, "name": "Feature A"})
|
||||
store.put(("flags",), "flag2", {"active": False, "name": "Feature B"})
|
||||
store.put(("flags",), "flag3", {"active": True, "name": "Feature C"})
|
||||
|
||||
# Test boolean filters
|
||||
results = store.search(("flags",), filter={"active": True})
|
||||
assert len(results) == 2
|
||||
assert all(r.value["active"] is True for r in results)
|
||||
|
||||
results = store.search(("flags",), filter={"active": False})
|
||||
assert len(results) == 1
|
||||
assert results[0].value["active"] is False
|
||||
|
||||
|
||||
def test_filter_keys_with_hyphens_and_digits(store: SqliteStore) -> None:
|
||||
"""Keys with hyphens or leading digits should be queryable via filters.
|
||||
|
||||
Current unquoted JSON path construction (e.g., '$.access-level' or '$.123abc')
|
||||
is not valid JSON1 syntax, so this test will catch regressions in path handling.
|
||||
"""
|
||||
# Documents with top-level and nested keys requiring bracket-quoted JSON paths
|
||||
store.put(
|
||||
("docs",),
|
||||
"hyphen",
|
||||
{"access-level": "public", "user": {"access-level": "nested"}},
|
||||
)
|
||||
store.put(("docs",), "digit", {"123abc": "ok", "user": {"123abc": "ok2"}})
|
||||
|
||||
# Top-level hyphenated key
|
||||
results = store.search(("docs",), filter={"access-level": "public"})
|
||||
assert [r.key for r in results] == ["hyphen"]
|
||||
|
||||
# Nested hyphenated key via dotted path
|
||||
results = store.search(("docs",), filter={"user.access-level": "nested"})
|
||||
assert [r.key for r in results] == ["hyphen"]
|
||||
|
||||
# Top-level digit-starting key
|
||||
results = store.search(("docs",), filter={"123abc": "ok"})
|
||||
assert [r.key for r in results] == ["digit"]
|
||||
|
||||
# Nested digit-starting key via dotted path
|
||||
results = store.search(("docs",), filter={"user.123abc": "ok2"})
|
||||
assert [r.key for r in results] == ["digit"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
|
||||
def test_non_ascii(
|
||||
fake_embeddings: CharacterEmbeddings,
|
||||
|
||||
Generated
+2
-2
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
revision = 2
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
@@ -293,7 +293,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "3.0.0"
|
||||
version = "3.0.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.langgraph_api/
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.4.8"
|
||||
__version__ = "0.4.11"
|
||||
|
||||
@@ -336,13 +336,13 @@ def _build(
|
||||
|
||||
# apply config
|
||||
stdin, additional_contexts = langgraph_cli.config.config_to_docker(
|
||||
config,
|
||||
config_json,
|
||||
base_image,
|
||||
api_version,
|
||||
install_command,
|
||||
build_command,
|
||||
build_context,
|
||||
config_path=config,
|
||||
config=config_json,
|
||||
base_image=base_image,
|
||||
api_version=api_version,
|
||||
install_command=install_command,
|
||||
build_command=build_command,
|
||||
build_context=build_context,
|
||||
)
|
||||
# add additional_contexts
|
||||
if additional_contexts:
|
||||
@@ -522,8 +522,8 @@ def dockerfile(
|
||||
|
||||
secho(f"📝 Generating Dockerfile at {save_path}", fg="yellow")
|
||||
dockerfile, additional_contexts = langgraph_cli.config.config_to_docker(
|
||||
config,
|
||||
config_json,
|
||||
config_path=config,
|
||||
config=config_json,
|
||||
base_image=base_image,
|
||||
api_version=api_version,
|
||||
)
|
||||
@@ -760,6 +760,7 @@ def dev(
|
||||
http=config_json.get("http"),
|
||||
ui=config_json.get("ui"),
|
||||
ui_config=config_json.get("ui_config"),
|
||||
webhooks=config_json.get("webhooks"),
|
||||
studio_url=studio_url,
|
||||
allow_blocking=allow_blocking,
|
||||
tunnel=tunnel,
|
||||
|
||||
@@ -824,6 +824,8 @@ def python_config_to_docker(
|
||||
config: Config,
|
||||
base_image: str,
|
||||
api_version: str | None = None,
|
||||
*,
|
||||
escape_variables: bool = False,
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
"""Generate a Dockerfile from the configuration."""
|
||||
pip_installer = config.get("pip_installer", "auto")
|
||||
@@ -1003,6 +1005,7 @@ ADD {relpath} /deps/{name}
|
||||
)
|
||||
|
||||
# Add main dockerfile content
|
||||
dep_vname = "$$dep" if escape_variables else "$dep"
|
||||
docker_file_contents.extend(
|
||||
[
|
||||
f"FROM {image_str}",
|
||||
@@ -1013,10 +1016,10 @@ ADD {relpath} /deps/{name}
|
||||
"",
|
||||
"# -- Installing all local dependencies --",
|
||||
f"""RUN for dep in /deps/*; do \
|
||||
echo "Installing $dep"; \
|
||||
if [ -d "$dep" ]; then \
|
||||
echo "Installing $dep"; \
|
||||
(cd "$dep" && {global_reqs_pip_install} -e .); \
|
||||
echo "Installing {dep_vname}"; \
|
||||
if [ -d "{dep_vname}" ]; then \
|
||||
echo "Installing {dep_vname}"; \
|
||||
(cd "{dep_vname}" && {global_reqs_pip_install} -e .); \
|
||||
fi; \
|
||||
done""",
|
||||
"# -- End of local dependencies install --",
|
||||
@@ -1202,26 +1205,34 @@ def _calculate_relative_workdir(config_path: pathlib.Path, build_context: str) -
|
||||
def config_to_docker(
|
||||
config_path: pathlib.Path,
|
||||
config: Config,
|
||||
*,
|
||||
base_image: str | None = None,
|
||||
api_version: str | None = None,
|
||||
install_command: str | None = None,
|
||||
build_command: str | None = None,
|
||||
build_context: str | None = None,
|
||||
escape_variables: bool = False,
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
base_image = base_image or default_base_image(config)
|
||||
|
||||
if config.get("node_version") and not config.get("python_version"):
|
||||
return node_config_to_docker(
|
||||
config_path,
|
||||
config,
|
||||
base_image,
|
||||
api_version,
|
||||
install_command,
|
||||
build_command,
|
||||
build_context,
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
base_image=base_image,
|
||||
api_version=api_version,
|
||||
install_command=install_command,
|
||||
build_command=build_command,
|
||||
build_context=build_context,
|
||||
)
|
||||
|
||||
return python_config_to_docker(config_path, config, base_image, api_version)
|
||||
return python_config_to_docker(
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
base_image=base_image,
|
||||
api_version=api_version,
|
||||
escape_variables=escape_variables,
|
||||
)
|
||||
|
||||
|
||||
def config_to_compose(
|
||||
@@ -1265,7 +1276,11 @@ def config_to_compose(
|
||||
|
||||
else:
|
||||
dockerfile, additional_contexts = config_to_docker(
|
||||
config_path, config, base_image, api_version
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
base_image=base_image,
|
||||
api_version=api_version,
|
||||
escape_variables=True,
|
||||
)
|
||||
|
||||
additional_contexts_str = "\n".join(
|
||||
|
||||
@@ -19,7 +19,7 @@ dependencies = [
|
||||
path = "langgraph_cli/__init__.py"
|
||||
[project.optional-dependencies]
|
||||
inmem = [
|
||||
"langgraph-api>=0.4,<0.6.0 ; python_version >= '3.11'",
|
||||
"langgraph-api>=0.5.35,<0.7.0 ; python_version >= '3.11'",
|
||||
"langgraph-runtime-inmem>=0.7 ; python_version >= '3.11'",
|
||||
"python-dotenv>=0.8.0",
|
||||
]
|
||||
@@ -49,6 +49,7 @@ lint = [
|
||||
dev = [
|
||||
{include-group = "test"},
|
||||
{include-group = "lint"},
|
||||
"hatch>=1.16.2",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
|
||||
@@ -150,7 +150,7 @@ services:
|
||||
COPY --from=cli_1 . /deps/cli_1
|
||||
# -- End of local package ../../.. --
|
||||
# -- Installing all local dependencies --
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
|
||||
RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||
|
||||
@@ -420,7 +420,7 @@ def test_config_to_docker_simple():
|
||||
"http": {"app": "../../examples/my_app.py:app"},
|
||||
}
|
||||
),
|
||||
"langchain/langgraph-api",
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = f"""\
|
||||
# syntax=docker/dockerfile:1.4
|
||||
@@ -483,7 +483,7 @@ def test_config_to_docker_outside_path():
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"dependencies": [".", ".."], "graphs": graphs}),
|
||||
"langchain/langgraph-api",
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = (
|
||||
"""\
|
||||
@@ -544,7 +544,7 @@ def test_config_to_docker_pipconfig():
|
||||
"pip_config_file": "pipconfig.txt",
|
||||
}
|
||||
),
|
||||
"langchain/langgraph-api",
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = (
|
||||
"""\
|
||||
@@ -585,7 +585,7 @@ def test_config_to_docker_invalid_inputs():
|
||||
config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"dependencies": ["./missing"], "graphs": graphs}),
|
||||
"langchain/langgraph-api",
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
|
||||
# test missing local module
|
||||
@@ -594,7 +594,7 @@ def test_config_to_docker_invalid_inputs():
|
||||
config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"dependencies": ["."], "graphs": graphs}),
|
||||
"langchain/langgraph-api",
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
|
||||
|
||||
@@ -608,7 +608,7 @@ def test_config_to_docker_local_deps():
|
||||
"graphs": graphs,
|
||||
}
|
||||
),
|
||||
"langchain/langgraph-api-custom",
|
||||
base_image="langchain/langgraph-api-custom",
|
||||
)
|
||||
expected_docker_stdin = f"""\
|
||||
FROM langchain/langgraph-api-custom:3.11
|
||||
@@ -654,7 +654,7 @@ dependencies = ["langchain"]"""
|
||||
"graphs": graphs,
|
||||
}
|
||||
),
|
||||
"langchain/langgraph-api",
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
os.remove(pyproject_path)
|
||||
expected_docker_stdin = (
|
||||
@@ -689,7 +689,7 @@ def test_config_to_docker_end_to_end():
|
||||
"dockerfile_lines": ["ARG meow", "ARG foo"],
|
||||
}
|
||||
),
|
||||
"langchain/langgraph-api",
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = f"""FROM langchain/langgraph-api:3.12
|
||||
ARG meow
|
||||
@@ -734,7 +734,7 @@ def test_config_to_docker_nodejs():
|
||||
"ui_config": {"shared": ["nuqs"]},
|
||||
}
|
||||
),
|
||||
"langchain/langgraphjs-api",
|
||||
base_image="langchain/langgraphjs-api",
|
||||
)
|
||||
expected_docker_stdin = """FROM langchain/langgraphjs-api:20
|
||||
ARG meow
|
||||
@@ -786,7 +786,7 @@ def test_config_to_docker_python_encryption_bad_path():
|
||||
def test_config_to_docker_python_encryption_formatted():
|
||||
# Test that encryption config is properly formatted in Docker output
|
||||
graphs = {"agent": "./graphs/agent.py:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
actual_docker_stdin, _ = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -796,7 +796,7 @@ def test_config_to_docker_python_encryption_formatted():
|
||||
"encryption": {"path": "./agent.py:my_encryption"},
|
||||
}
|
||||
),
|
||||
"langchain/langgraph-api",
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
# Verify that LANGGRAPH_ENCRYPTION is in the docker output with the correct path
|
||||
assert "LANGGRAPH_ENCRYPTION=" in actual_docker_stdin
|
||||
@@ -821,7 +821,7 @@ def test_config_to_docker_nodejs_internal_docker_tag():
|
||||
"_INTERNAL_docker_tag": "my-tag",
|
||||
}
|
||||
),
|
||||
"langchain/langgraphjs-api",
|
||||
base_image="langchain/langgraphjs-api",
|
||||
)
|
||||
expected_docker_stdin = """FROM langchain/langgraphjs-api:my-tag
|
||||
ARG meow
|
||||
@@ -875,7 +875,7 @@ def test_config_to_docker_webhooks_python():
|
||||
"webhooks": webhooks,
|
||||
}
|
||||
),
|
||||
"langchain/langgraph-api",
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
|
||||
# Ensure the ENV line is present and the payload round-trips via JSON
|
||||
@@ -900,7 +900,7 @@ def test_config_to_docker_webhooks_node():
|
||||
"webhooks": webhooks,
|
||||
}
|
||||
),
|
||||
"langchain/langgraphjs-api",
|
||||
base_image="langchain/langgraphjs-api",
|
||||
)
|
||||
|
||||
parsed = _extract_env_json(dockerfile, "LANGGRAPH_WEBHOOKS")
|
||||
@@ -912,7 +912,7 @@ def test_config_to_docker_no_webhooks():
|
||||
dockerfile, _ = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"dependencies": ["."], "graphs": graphs}),
|
||||
"langchain/langgraph-api",
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
|
||||
assert "ENV LANGGRAPH_WEBHOOKS=" not in dockerfile
|
||||
@@ -930,7 +930,7 @@ def test_config_to_docker_gen_ui_python():
|
||||
"ui_config": {"shared": ["nuqs"]},
|
||||
}
|
||||
),
|
||||
"langchain/langgraph-api",
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
|
||||
expected_docker_stdin = f"""FROM langchain/langgraph-api:3.11
|
||||
@@ -976,7 +976,7 @@ def test_config_to_docker_multiplatform():
|
||||
validate_config(
|
||||
{"node_version": "22", "dependencies": ["."], "graphs": graphs}
|
||||
),
|
||||
"langchain/langgraph-api",
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
|
||||
expected_docker_stdin = f"""FROM langchain/langgraph-api:3.11
|
||||
@@ -1024,7 +1024,7 @@ def test_config_to_docker_pip_installer():
|
||||
{**copy.deepcopy(base_config), "pip_installer": "auto"}
|
||||
)
|
||||
docker_auto, _ = config_to_docker(
|
||||
PATH_TO_CONFIG, config_auto, "langchain/langgraph-api:0.2.47"
|
||||
PATH_TO_CONFIG, config_auto, base_image="langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert "uv pip install --system " in docker_auto
|
||||
assert "rm /usr/bin/uv /usr/bin/uvx" in docker_auto
|
||||
@@ -1032,7 +1032,7 @@ def test_config_to_docker_pip_installer():
|
||||
# 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"
|
||||
PATH_TO_CONFIG, config_pip, base_image="langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert "uv pip install --system " not in docker_pip
|
||||
assert "pip install" in docker_pip
|
||||
@@ -1041,7 +1041,7 @@ def test_config_to_docker_pip_installer():
|
||||
# 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"
|
||||
PATH_TO_CONFIG, config_uv, base_image="langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert "uv pip install --system " in docker_uv
|
||||
assert "rm /usr/bin/uv /usr/bin/uvx" in docker_uv
|
||||
@@ -1051,7 +1051,7 @@ def test_config_to_docker_pip_installer():
|
||||
{**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"
|
||||
PATH_TO_CONFIG, config_auto_old, base_image="langchain/langgraph-api:0.2.46"
|
||||
)
|
||||
assert "uv pip install --system " not in docker_auto_old
|
||||
assert "pip install" in docker_auto_old
|
||||
@@ -1060,7 +1060,7 @@ def test_config_to_docker_pip_installer():
|
||||
# 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"
|
||||
PATH_TO_CONFIG, config_default, base_image="langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert "uv pip install --system " in docker_default
|
||||
|
||||
@@ -1076,7 +1076,7 @@ def test_config_retain_build_tools():
|
||||
{**copy.deepcopy(base_config), "keep_pkg_tools": True}
|
||||
)
|
||||
docker_true, _ = config_to_docker(
|
||||
PATH_TO_CONFIG, config_true, "langchain/langgraph-api:0.2.47"
|
||||
PATH_TO_CONFIG, config_true, base_image="langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert not any(
|
||||
"/usr/local/lib/python*/site-packages/" + pckg + "*" in docker_true
|
||||
@@ -1087,7 +1087,7 @@ def test_config_retain_build_tools():
|
||||
{**copy.deepcopy(base_config), "keep_pkg_tools": False}
|
||||
)
|
||||
docker_false, _ = config_to_docker(
|
||||
PATH_TO_CONFIG, config_false, "langchain/langgraph-api:0.2.47"
|
||||
PATH_TO_CONFIG, config_false, base_image="langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert all(
|
||||
"/usr/local/lib/python*/site-packages/" + pckg + "*" in docker_false
|
||||
@@ -1098,7 +1098,7 @@ def test_config_retain_build_tools():
|
||||
{**copy.deepcopy(base_config), "keep_pkg_tools": ["pip", "setuptools"]}
|
||||
)
|
||||
docker_list, _ = config_to_docker(
|
||||
PATH_TO_CONFIG, config_list, "langchain/langgraph-api:0.2.47"
|
||||
PATH_TO_CONFIG, config_list, base_image="langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert all(
|
||||
"/usr/local/lib/python*/site-packages/" + pckg + "*" in docker_list
|
||||
@@ -1137,7 +1137,7 @@ def test_config_to_compose_simple_config():
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
|
||||
RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||
@@ -1178,7 +1178,7 @@ def test_config_to_compose_env_vars():
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
|
||||
RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||
@@ -1223,7 +1223,7 @@ def test_config_to_compose_env_file():
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
|
||||
RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||
@@ -1261,7 +1261,7 @@ def test_config_to_compose_watch():
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
|
||||
RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||
@@ -1308,7 +1308,7 @@ def test_config_to_compose_end_to_end():
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN for dep in /deps/*; do echo "Installing $dep"; if [ -d "$dep" ]; then echo "Installing $dep"; (cd "$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
|
||||
RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/outer-unit_tests/unit_tests/agent.py:graph"}}'
|
||||
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||
@@ -1619,7 +1619,7 @@ def test_config_to_docker_with_api_version():
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"dependencies": ["."], "graphs": graphs}),
|
||||
"langchain/langgraph-api",
|
||||
base_image="langchain/langgraph-api",
|
||||
api_version="0.2.74",
|
||||
)
|
||||
|
||||
@@ -1633,7 +1633,7 @@ def test_config_to_docker_with_api_version():
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"node_version": "20", "graphs": graphs}),
|
||||
"langchain/langgraphjs-api",
|
||||
base_image="langchain/langgraphjs-api",
|
||||
api_version="0.2.74",
|
||||
)
|
||||
|
||||
|
||||
Generated
+725
-260
File diff suppressed because it is too large
Load Diff
+33
-26
@@ -11,7 +11,7 @@
|
||||
[](https://pypi.org/project/langgraph/)
|
||||
[](https://pepy.tech/project/langgraph)
|
||||
[](https://github.com/langchain-ai/langgraph/issues)
|
||||
[](https://langchain-ai.github.io/langgraph/)
|
||||
[](https://docs.langchain.com/oss/python/langgraph/overview)
|
||||
|
||||
Trusted by companies shaping the future of agents – including Klarna, Replit, Elastic, and more – LangGraph is a low-level orchestration framework for building, managing, and deploying long-running, stateful agents.
|
||||
|
||||
@@ -23,47 +23,55 @@ Install LangGraph:
|
||||
pip install -U langgraph
|
||||
```
|
||||
|
||||
Then, create an agent [using prebuilt components](https://langchain-ai.github.io/langgraph/agents/agents/):
|
||||
Create a simple workflow:
|
||||
|
||||
```python
|
||||
# pip install -qU "langchain[anthropic]" to call the model
|
||||
from langgraph.graph import START, StateGraph
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
def get_weather(city: str) -> str:
|
||||
"""Get weather for a given city."""
|
||||
return f"It's always sunny in {city}!"
|
||||
class State(TypedDict):
|
||||
text: str
|
||||
|
||||
agent = create_react_agent(
|
||||
model="anthropic:claude-3-7-sonnet-latest",
|
||||
tools=[get_weather],
|
||||
prompt="You are a helpful assistant"
|
||||
)
|
||||
|
||||
# Run the agent
|
||||
agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]}
|
||||
)
|
||||
def node_a(state: State) -> dict:
|
||||
return {"text": state["text"] + "a"}
|
||||
|
||||
|
||||
def node_b(state: State) -> dict:
|
||||
return {"text": state["text"] + "b"}
|
||||
|
||||
|
||||
graph = StateGraph(State)
|
||||
graph.add_node("node_a", node_a)
|
||||
graph.add_node("node_b", node_b)
|
||||
graph.add_edge(START, "node_a")
|
||||
graph.add_edge("node_a", "node_b")
|
||||
|
||||
print(graph.compile().invoke({"text": ""}))
|
||||
# {'text': 'ab'}
|
||||
```
|
||||
|
||||
For more information, see the [Quickstart](https://langchain-ai.github.io/langgraph/agents/agents/). Or, to learn how to build an [agent workflow](https://langchain-ai.github.io/langgraph/concepts/low_level/) with a customizable architecture, long-term memory, and other complex task handling, see the [LangGraph basics tutorials](https://langchain-ai.github.io/langgraph/tutorials/get-started/1-build-basic-chatbot/).
|
||||
Get started with the [LangGraph Quickstart](https://docs.langchain.com/oss/python/langgraph/quickstart).
|
||||
|
||||
To quickly build agents with LangChain's `create_agent` (built on LangGraph), see the [LangChain Agents documentation](https://docs.langchain.com/oss/python/langchain/agents).
|
||||
|
||||
## Core benefits
|
||||
|
||||
LangGraph provides low-level supporting infrastructure for *any* long-running, stateful workflow or agent. LangGraph does not abstract prompts or architecture, and provides the following central benefits:
|
||||
|
||||
- [Durable execution](https://langchain-ai.github.io/langgraph/concepts/durable_execution/): Build agents that persist through failures and can run for extended periods, automatically resuming from exactly where they left off.
|
||||
- [Human-in-the-loop](https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/): Seamlessly incorporate human oversight by inspecting and modifying agent state at any point during execution.
|
||||
- [Comprehensive memory](https://langchain-ai.github.io/langgraph/concepts/memory/): Create truly stateful agents with both short-term working memory for ongoing reasoning and long-term persistent memory across sessions.
|
||||
- [Durable execution](https://docs.langchain.com/oss/python/langgraph/durable-execution): Build agents that persist through failures and can run for extended periods, automatically resuming from exactly where they left off.
|
||||
- [Human-in-the-loop](https://docs.langchain.com/oss/python/langgraph/interrupts): Seamlessly incorporate human oversight by inspecting and modifying agent state at any point during execution.
|
||||
- [Comprehensive memory](https://docs.langchain.com/oss/python/langgraph/memory): Create truly stateful agents with both short-term working memory for ongoing reasoning and long-term persistent memory across sessions.
|
||||
- [Debugging with LangSmith](http://www.langchain.com/langsmith): Gain deep visibility into complex agent behavior with visualization tools that trace execution paths, capture state transitions, and provide detailed runtime metrics.
|
||||
- [Production-ready deployment](https://langchain-ai.github.io/langgraph/concepts/deployment_options/): Deploy sophisticated agent systems confidently with scalable infrastructure designed to handle the unique challenges of stateful, long-running workflows.
|
||||
- [Production-ready deployment](https://docs.langchain.com/langsmith/app-development): Deploy sophisticated agent systems confidently with scalable infrastructure designed to handle the unique challenges of stateful, long-running workflows.
|
||||
|
||||
## LangGraph’s ecosystem
|
||||
|
||||
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
|
||||
|
||||
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
|
||||
- [LangSmith Deployment](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
|
||||
- [LangSmith Deployment](https://docs.langchain.com/langsmith/deployments) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://docs.langchain.com/oss/python/langgraph/studio).
|
||||
- [LangChain](https://docs.langchain.com/oss/python/langchain/overview) – Provides integrations and composable components to streamline LLM application development.
|
||||
|
||||
> [!NOTE]
|
||||
@@ -71,12 +79,11 @@ While LangGraph can be used standalone, it also integrates seamlessly with any L
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [Guides](https://langchain-ai.github.io/langgraph/guides/): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
|
||||
- [Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
|
||||
- [Examples](https://langchain-ai.github.io/langgraph/examples/): Guided examples on getting started with LangGraph.
|
||||
- [Guides](https://docs.langchain.com/oss/python/langgraph/guides): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
|
||||
- [Reference](https://reference.langchain.com/python/langgraph/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
|
||||
- [Examples](https://docs.langchain.com/oss/python/langgraph/agentic-rag): Guided examples on getting started with LangGraph.
|
||||
- [LangChain Forum](https://forum.langchain.com/): Connect with the community and share all of your technical questions, ideas, and feedback.
|
||||
- [LangChain Academy](https://academy.langchain.com/courses/intro-to-langgraph): Learn the basics of LangGraph in our free, structured course.
|
||||
- [Templates](https://langchain-ai.github.io/langgraph/concepts/template_applications/): Pre-built reference apps for common agentic workflows (e.g. ReAct agent, memory, retrieval etc.) that can be cloned and adapted.
|
||||
- [Case studies](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship AI applications at scale.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
@@ -78,6 +78,7 @@ from langgraph.types import (
|
||||
Command,
|
||||
RetryPolicy,
|
||||
Send,
|
||||
ensure_valid_checkpointer,
|
||||
)
|
||||
from langgraph.typing import ContextT, InputT, NodeInputT, OutputT, StateT
|
||||
from langgraph.warnings import LangGraphDeprecatedSinceV05, LangGraphDeprecatedSinceV10
|
||||
@@ -853,6 +854,8 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
Returns:
|
||||
CompiledStateGraph: The compiled `StateGraph`.
|
||||
"""
|
||||
checkpointer = ensure_valid_checkpointer(checkpointer)
|
||||
|
||||
# assign default values
|
||||
interrupt_before = interrupt_before or []
|
||||
interrupt_after = interrupt_after or []
|
||||
|
||||
@@ -142,6 +142,7 @@ from langgraph.types import (
|
||||
StateSnapshot,
|
||||
StateUpdate,
|
||||
StreamMode,
|
||||
ensure_valid_checkpointer,
|
||||
)
|
||||
from langgraph.typing import ContextT, InputT, OutputT, StateT
|
||||
from langgraph.warnings import LangGraphDeprecatedSinceV10
|
||||
@@ -642,7 +643,7 @@ class Pregel(
|
||||
input_channels: str | Sequence[str],
|
||||
step_timeout: float | None = None,
|
||||
debug: bool | None = None,
|
||||
checkpointer: BaseCheckpointSaver | None = None,
|
||||
checkpointer: Checkpointer = None,
|
||||
store: BaseStore | None = None,
|
||||
cache: BaseCache | None = None,
|
||||
retry_policy: RetryPolicy | Sequence[RetryPolicy] = (),
|
||||
@@ -665,6 +666,8 @@ class Pregel(
|
||||
if context_schema is None:
|
||||
context_schema = cast(type[ContextT], config_type)
|
||||
|
||||
checkpointer = ensure_valid_checkpointer(checkpointer)
|
||||
|
||||
self.nodes = {
|
||||
k: v.build() if isinstance(v, NodeBuilder) else v for k, v in nodes.items()
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ __all__ = (
|
||||
"Durability",
|
||||
"interrupt",
|
||||
"Overwrite",
|
||||
"ensure_valid_checkpointer",
|
||||
)
|
||||
|
||||
Durability = Literal["sync", "async", "exit"]
|
||||
@@ -73,6 +74,20 @@ Checkpointer = None | bool | BaseCheckpointSaver
|
||||
- False disables checkpointing, even if the parent graph has a checkpointer.
|
||||
- None inherits checkpointer from the parent graph."""
|
||||
|
||||
|
||||
def ensure_valid_checkpointer(checkpointer: Checkpointer) -> Checkpointer:
|
||||
if checkpointer not in (None, True, False) and not isinstance(
|
||||
checkpointer, BaseCheckpointSaver
|
||||
):
|
||||
raise TypeError(
|
||||
"Invalid checkpointer provided. Expected an instance of "
|
||||
"`BaseCheckpointSaver`, `True`, `False`, or `None`. "
|
||||
f"Received {type(checkpointer).__name__!s}. "
|
||||
"Pass a proper saver (e.g., InMemorySaver, AsyncPostgresSaver)."
|
||||
)
|
||||
return checkpointer
|
||||
|
||||
|
||||
StreamMode = Literal[
|
||||
"values", "updates", "checkpoints", "tasks", "debug", "messages", "custom"
|
||||
]
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph"
|
||||
version = "1.0.4"
|
||||
version = "1.0.5"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
requires-python = ">=3.10"
|
||||
@@ -26,7 +26,7 @@ classifiers = [
|
||||
dependencies = [
|
||||
"langchain-core>=0.1",
|
||||
"langgraph-checkpoint>=2.1.0,<4.0.0",
|
||||
"langgraph-sdk>=0.2.2,<0.3.0",
|
||||
"langgraph-sdk>=0.3.0,<0.4.0",
|
||||
"langgraph-prebuilt>=1.0.2,<1.1.0",
|
||||
"xxhash>=3.5.0",
|
||||
"pydantic>=2.7.4",
|
||||
|
||||
@@ -120,6 +120,22 @@ def test_graph_validation() -> None:
|
||||
graph.invoke({"hello": "there"})
|
||||
|
||||
|
||||
def test_invalid_checkpointer_type() -> None:
|
||||
class State(TypedDict):
|
||||
foo: str
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("start", lambda state: state)
|
||||
builder.set_entry_point("start")
|
||||
builder.set_finish_point("start")
|
||||
|
||||
class NotACheckpointer:
|
||||
pass
|
||||
|
||||
with pytest.raises(TypeError, match="Invalid checkpointer provided"):
|
||||
builder.compile(checkpointer=NotACheckpointer())
|
||||
|
||||
|
||||
def test_graph_validation_with_command() -> None:
|
||||
class State(TypedDict):
|
||||
foo: str
|
||||
|
||||
@@ -567,9 +567,9 @@ def test_stream():
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
("updates", {"chunk": "data3"}),
|
||||
("updates", {"chunk": "data4"}),
|
||||
("updates", {"__interrupt__": ()}),
|
||||
("updates", {"chunk": "data3"}, None),
|
||||
("updates", {"chunk": "data4"}, None),
|
||||
("updates", {"__interrupt__": ()}, None),
|
||||
]
|
||||
|
||||
# subgraphs + list modes
|
||||
@@ -739,9 +739,9 @@ async def test_astream():
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
("updates", {"chunk": "data3"}),
|
||||
("updates", {"chunk": "data4"}),
|
||||
("updates", {"__interrupt__": ()}),
|
||||
("updates", {"chunk": "data3"}, None),
|
||||
("updates", {"chunk": "data4"}, None),
|
||||
("updates", {"__interrupt__": ()}, None),
|
||||
]
|
||||
|
||||
# subgraphs + list modes
|
||||
|
||||
Generated
+77
-76
@@ -195,7 +195,7 @@ name = "blockbuster"
|
||||
version = "1.5.26"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "forbiddenfruit", marker = "python_full_version >= '3.11' and implementation_name == 'cpython'" },
|
||||
{ name = "forbiddenfruit", marker = "python_full_version >= '3.11' and python_full_version < '3.14' and implementation_name == 'cpython'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e0/dcbab602790a576b0b94108c07e2c048e5897df7cc83722a89582d733987/blockbuster-1.5.26.tar.gz", hash = "sha256:cc3ce8c70fa852a97ee3411155f31e4ad2665cd1c6c7d2f8bb1851dab61dc629", size = 36085, upload-time = "2025-12-05T10:43:47.735Z" }
|
||||
wheels = [
|
||||
@@ -387,7 +387,7 @@ name = "click"
|
||||
version = "8.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "colorama", marker = "python_full_version < '3.14' and sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
||||
wheels = [
|
||||
@@ -530,7 +530,7 @@ name = "cryptography"
|
||||
version = "44.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "python_full_version >= '3.11' and platform_python_implementation != 'PyPy'" },
|
||||
{ name = "cffi", marker = "python_full_version >= '3.11' and python_full_version < '3.14' and platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/d6/1411ab4d6108ab167d06254c5be517681f1e331f90edf1379895bcb87020/cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053", size = 711096, upload-time = "2025-05-02T19:36:04.667Z" }
|
||||
wheels = [
|
||||
@@ -678,7 +678,7 @@ name = "googleapis-common-protos"
|
||||
version = "1.72.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" }
|
||||
wheels = [
|
||||
@@ -690,7 +690,7 @@ name = "grpcio"
|
||||
version = "1.76.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.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
|
||||
wheels = [
|
||||
@@ -751,9 +751,9 @@ name = "grpcio-tools"
|
||||
version = "1.75.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "grpcio", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "setuptools", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "grpcio", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "setuptools", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/76/0cd2a2bb379275c319544a3ab613dc3cea7a167503908c1b4de55f82bd9e/grpcio_tools-1.75.1.tar.gz", hash = "sha256:bb78960cf3d58941e1fec70cbdaccf255918beed13c34112a6915a6d8facebd1", size = 5390470, upload-time = "2025-09-26T09:10:11.948Z" }
|
||||
wheels = [
|
||||
@@ -860,7 +860,7 @@ name = "importlib-metadata"
|
||||
version = "8.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "zipp", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "zipp", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
|
||||
wheels = [
|
||||
@@ -1345,7 +1345,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "1.0.4"
|
||||
version = "1.0.5"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -1488,39 +1488,39 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-api"
|
||||
version = "0.5.30"
|
||||
version = "0.5.35"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cloudpickle", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "cryptography", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "grpcio", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "grpcio-tools", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "httpx", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "jsonschema-rs", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langchain-core", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph-checkpoint", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langsmith", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-sdk", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "orjson", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "pyjwt", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sse-starlette", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "starlette", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "structlog", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "tenacity", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "truststore", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "uuid-utils", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "uvicorn", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "watchfiles", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "cloudpickle", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "cryptography", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "grpcio", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "grpcio-tools", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "httpx", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "jsonschema-rs", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langchain-core", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langgraph", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langgraph-checkpoint", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langsmith", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-http", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-sdk", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "orjson", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "pyjwt", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "sse-starlette", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "starlette", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "structlog", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "tenacity", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "truststore", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "uuid-utils", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "uvicorn", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "watchfiles", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/67/fb/0f75ac52d7aa9bf9c001b7d36e1515a849904a9c7acc51f4ca5b8bd873f4/langgraph_api-0.5.30.tar.gz", hash = "sha256:f95f9102ca9b8a1716be7c57d7812344c785c9a1785b3bda84f1c30517c5fbec", size = 367716, upload-time = "2025-12-05T04:04:08.557Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/27/4dd4287ec65690e3a212d7154e20504b4e88e861fd62625053be8903bc57/langgraph_api-0.5.35.tar.gz", hash = "sha256:b5687a5201ff365e1bc016042a7103ed8a2c2440f57b71f8480c223585bbfca1", size = 378029, upload-time = "2025-12-09T00:37:35.091Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/b3/9ca17fa9417d885ee9f5d22e4629029ae848282fcf6dac9cdbe5e0b0ec71/langgraph_api-0.5.30-py3-none-any.whl", hash = "sha256:aa2d9fedc3d1c9394bd1534265f107bcb508e31e4f029fad1795489178d1adf2", size = 295086, upload-time = "2025-12-05T04:04:07.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/80/296db2db262a90b0fe3cb2562790025e018e33da9d171cc64f12076e5911/langgraph_api-0.5.35-py3-none-any.whl", hash = "sha256:6aaf967c52ff719861b80e4dc8066968baa185d9daae8433f0d84b5a27708a65", size = 305523, upload-time = "2025-12-09T00:37:34.001Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1572,7 +1572,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "3.0.1"
|
||||
version = "3.0.2"
|
||||
source = { editable = "../checkpoint-postgres" }
|
||||
dependencies = [
|
||||
{ name = "langgraph-checkpoint" },
|
||||
@@ -1619,7 +1619,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "3.0.0"
|
||||
version = "3.0.1"
|
||||
source = { editable = "../checkpoint-sqlite" }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
@@ -1664,21 +1664,21 @@ test = [
|
||||
name = "langgraph-cli"
|
||||
source = { editable = "../cli" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "click", marker = "python_full_version < '3.14'" },
|
||||
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
inmem = [
|
||||
{ name = "langgraph-api", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "python-dotenv", marker = "python_full_version < '3.14'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "click", specifier = ">=8.1.7" },
|
||||
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.4,<0.6.0" },
|
||||
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.5.35,<0.7.0" },
|
||||
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.7" },
|
||||
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11'", specifier = ">=0.1.0" },
|
||||
{ name = "python-dotenv", marker = "extra == 'inmem'", specifier = ">=0.8.0" },
|
||||
@@ -1688,6 +1688,7 @@ provides-extras = ["inmem"]
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "codespell" },
|
||||
{ name = "hatch", specifier = ">=1.16.2" },
|
||||
{ name = "msgspec" },
|
||||
{ name = "mypy" },
|
||||
{ name = "pytest" },
|
||||
@@ -1765,12 +1766,12 @@ name = "langgraph-runtime-inmem"
|
||||
version = "0.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "blockbuster", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph-checkpoint", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sse-starlette", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "starlette", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "structlog", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "blockbuster", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langgraph", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "langgraph-checkpoint", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "sse-starlette", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "starlette", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "structlog", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/9e/6e7b321ef02834059983d6d5a635cc20f9987b19fe6a4666332c8b9b0ede/langgraph_runtime_inmem-0.19.1.tar.gz", hash = "sha256:573d576cf38392fcace76d772be9adc4d54b2af129ae54cb9780bab4fb55ee69", size = 98975, upload-time = "2025-12-04T07:01:40.105Z" }
|
||||
wheels = [
|
||||
@@ -2180,8 +2181,8 @@ name = "opentelemetry-api"
|
||||
version = "1.39.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "importlib-metadata", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "importlib-metadata", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/0b/e5428c009d4d9af0515b0a8371a8aaae695371af291f45e702f7969dce6b/opentelemetry_api-1.39.0.tar.gz", hash = "sha256:6130644268c5ac6bdffaf660ce878f10906b3e789f7e2daa5e169b047a2933b9", size = 65763, upload-time = "2025-12-03T13:19:56.378Z" }
|
||||
wheels = [
|
||||
@@ -2193,7 +2194,7 @@ name = "opentelemetry-exporter-otlp-proto-common"
|
||||
version = "1.39.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-proto", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-proto", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/cb/3a29ce606b10c76d413d6edd42d25a654af03e73e50696611e757d2602f3/opentelemetry_exporter_otlp_proto_common-1.39.0.tar.gz", hash = "sha256:a135fceed1a6d767f75be65bd2845da344dd8b9258eeed6bc48509d02b184409", size = 20407, upload-time = "2025-12-03T13:19:59.003Z" }
|
||||
wheels = [
|
||||
@@ -2205,13 +2206,13 @@ name = "opentelemetry-exporter-otlp-proto-http"
|
||||
version = "1.39.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "googleapis-common-protos", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-common", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-proto", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-sdk", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "requests", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "googleapis-common-protos", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-common", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-proto", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-sdk", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "requests", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/dc/1e9bf3f6a28e29eba516bc0266e052996d02bc7e92675f3cd38169607609/opentelemetry_exporter_otlp_proto_http-1.39.0.tar.gz", hash = "sha256:28d78fc0eb82d5a71ae552263d5012fa3ebad18dfd189bf8d8095ba0e65ee1ed", size = 17287, upload-time = "2025-12-03T13:20:01.134Z" }
|
||||
wheels = [
|
||||
@@ -2223,7 +2224,7 @@ name = "opentelemetry-proto"
|
||||
version = "1.39.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/b5/64d2f8c3393cd13ea2092106118f7b98461ba09333d40179a31444c6f176/opentelemetry_proto-1.39.0.tar.gz", hash = "sha256:c1fa48678ad1a1624258698e59be73f990b7fc1f39e73e16a9d08eef65dd838c", size = 46153, upload-time = "2025-12-03T13:20:08.729Z" }
|
||||
wheels = [
|
||||
@@ -2235,9 +2236,9 @@ name = "opentelemetry-sdk"
|
||||
version = "1.39.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/51/e3/7cd989003e7cde72e0becfe830abff0df55c69d237ee7961a541e0167833/opentelemetry_sdk-1.39.0.tar.gz", hash = "sha256:c22204f12a0529e07aa4d985f1bca9d6b0e7b29fe7f03e923548ae52e0e15dde", size = 171322, upload-time = "2025-12-03T13:20:09.651Z" }
|
||||
wheels = [
|
||||
@@ -2249,8 +2250,8 @@ name = "opentelemetry-semantic-conventions"
|
||||
version = "0.60b0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/0e/176a7844fe4e3cb5de604212094dffaed4e18b32f1c56b5258bcbcba85c2/opentelemetry_semantic_conventions-0.60b0.tar.gz", hash = "sha256:227d7aa73cbb8a2e418029d6b6465553aa01cf7e78ec9d0bc3255c7b3ac5bf8f", size = 137935, upload-time = "2025-12-03T13:20:12.395Z" }
|
||||
wheels = [
|
||||
@@ -3431,9 +3432,9 @@ name = "sse-starlette"
|
||||
version = "2.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "starlette", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "uvicorn", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "starlette", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "uvicorn", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/fc/56ab9f116b2133521f532fce8d03194cf04dcac25f583cf3d839be4c0496/sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169", size = 19678, upload-time = "2024-08-01T08:52:50.248Z" }
|
||||
wheels = [
|
||||
@@ -3459,7 +3460,7 @@ name = "starlette"
|
||||
version = "0.50.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" }
|
||||
@@ -3703,8 +3704,8 @@ name = "uvicorn"
|
||||
version = "0.38.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "h11", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "click", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "h11", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" }
|
||||
wheels = [
|
||||
@@ -3780,7 +3781,7 @@ name = "watchfiles"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
|
||||
wheels = [
|
||||
|
||||
@@ -638,7 +638,7 @@ def test_react_agent_parallel_tool_calls(
|
||||
for event in agent.stream(
|
||||
{"messages": [("user", query)]}, config, stream_mode="values"
|
||||
):
|
||||
if "__interrupt__" not in event:
|
||||
if "__interrupt__" not in event:
|
||||
if messages := event.get("messages"):
|
||||
message_types.append([m.type for m in messages])
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -271,7 +271,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "1.0.4"
|
||||
version = "1.0.5"
|
||||
source = { editable = "../langgraph" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -402,7 +402,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "3.0.1"
|
||||
version = "3.0.2"
|
||||
source = { editable = "../checkpoint-postgres" }
|
||||
dependencies = [
|
||||
{ name = "langgraph-checkpoint" },
|
||||
@@ -449,7 +449,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "3.0.0"
|
||||
version = "3.0.1"
|
||||
source = { editable = "../checkpoint-sqlite" }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
|
||||
@@ -3,6 +3,6 @@ from langgraph_sdk.client import get_client, get_sync_client
|
||||
from langgraph_sdk.encryption import Encryption
|
||||
from langgraph_sdk.encryption.types import EncryptionContext
|
||||
|
||||
__version__ = "0.2.14"
|
||||
__version__ = "0.3.0"
|
||||
|
||||
__all__ = ["Auth", "Encryption", "EncryptionContext", "get_client", "get_sync_client"]
|
||||
|
||||
@@ -1193,6 +1193,7 @@ class AssistantsClient:
|
||||
*,
|
||||
metadata: Json = None,
|
||||
graph_id: str | None = None,
|
||||
name: str | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> int:
|
||||
@@ -1201,6 +1202,8 @@ class AssistantsClient:
|
||||
Args:
|
||||
metadata: Metadata to filter by. Exact match for each key/value.
|
||||
graph_id: Optional graph id to filter by.
|
||||
name: Optional name to filter by.
|
||||
The filtering logic will match assistants where 'name' is a substring (case insensitive) of the assistant name.
|
||||
headers: Optional custom headers to include with the request.
|
||||
params: Optional query parameters to include with the request.
|
||||
|
||||
@@ -1212,6 +1215,8 @@ class AssistantsClient:
|
||||
payload["metadata"] = metadata
|
||||
if graph_id:
|
||||
payload["graph_id"] = graph_id
|
||||
if name:
|
||||
payload["name"] = name
|
||||
return await self.http.post(
|
||||
"/assistants/count", json=payload, headers=headers, params=params
|
||||
)
|
||||
@@ -4526,6 +4531,7 @@ class SyncAssistantsClient:
|
||||
*,
|
||||
metadata: Json = None,
|
||||
graph_id: str | None = None,
|
||||
name: str | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> int:
|
||||
@@ -4534,6 +4540,8 @@ class SyncAssistantsClient:
|
||||
Args:
|
||||
metadata: Metadata to filter by. Exact match for each key/value.
|
||||
graph_id: Optional graph id to filter by.
|
||||
name: Optional name to filter by.
|
||||
The filtering logic will match assistants where 'name' is a substring (case insensitive) of the assistant name.
|
||||
headers: Optional custom headers to include with the request.
|
||||
params: Optional query parameters to include with the request.
|
||||
|
||||
@@ -4545,6 +4553,8 @@ class SyncAssistantsClient:
|
||||
payload["metadata"] = metadata
|
||||
if graph_id:
|
||||
payload["graph_id"] = graph_id
|
||||
if name:
|
||||
payload["name"] = name
|
||||
return self.http.post(
|
||||
"/assistants/count", json=payload, headers=headers, params=params
|
||||
)
|
||||
|
||||
@@ -167,7 +167,7 @@ class Config(TypedDict, total=False):
|
||||
"""
|
||||
Runtime values for attributes previously made configurable on this Runnable,
|
||||
or sub-Runnables, through .configurable_fields() or .configurable_alternatives().
|
||||
Check .output_schema() for a description of the attributes that have been made
|
||||
Check .output_schema() for a description of the attributes that have been made
|
||||
configurable.
|
||||
"""
|
||||
|
||||
@@ -301,7 +301,7 @@ class ThreadState(TypedDict):
|
||||
values: list[dict] | dict[str, Any]
|
||||
"""The state values."""
|
||||
next: Sequence[str]
|
||||
"""The next nodes to execute. If empty, the thread is done until new input is
|
||||
"""The next nodes to execute. If empty, the thread is done until new input is
|
||||
received."""
|
||||
checkpoint: Checkpoint
|
||||
"""The ID of the checkpoint."""
|
||||
@@ -476,7 +476,7 @@ class Item(TypedDict):
|
||||
"""The namespace of the item. A namespace is analogous to a document's directory."""
|
||||
key: str
|
||||
"""The unique identifier of the item within its namespace.
|
||||
|
||||
|
||||
In general, keys needn't be globally unique.
|
||||
"""
|
||||
value: dict[str, Any]
|
||||
@@ -519,6 +519,8 @@ class StreamPart(NamedTuple):
|
||||
"""The type of event for this stream part."""
|
||||
data: dict
|
||||
"""The data payload associated with the event."""
|
||||
id: str | None = None
|
||||
"""The ID of the event."""
|
||||
|
||||
|
||||
class Send(TypedDict):
|
||||
|
||||
@@ -103,6 +103,7 @@ class SSEDecoder:
|
||||
sse = StreamPart(
|
||||
event=self._event,
|
||||
data=orjson.loads(self._data) if self._data else None, # type: ignore[invalid-argument-type]
|
||||
id=self.last_event_id,
|
||||
)
|
||||
|
||||
# NOTE: as per the SSE spec, do not reset last_event_id.
|
||||
|
||||
@@ -50,7 +50,7 @@ def iter_lines_raw(payload: list[bytes]) -> Iterator[BytesLike]:
|
||||
yield from decoder.flush()
|
||||
|
||||
|
||||
def test_stream_see():
|
||||
def test_stream_sse():
|
||||
for groups in (
|
||||
[RESPONSE_PAYLOAD],
|
||||
RESPONSE_PAYLOAD.splitlines(keepends=True),
|
||||
@@ -150,9 +150,9 @@ def test_sync_http_client_stream_recovers_after_disconnect():
|
||||
|
||||
assert call_count == 2
|
||||
assert parts == [
|
||||
StreamPart(event="values", data={"step": 1}),
|
||||
StreamPart(event="values", data={"step": 2}),
|
||||
StreamPart(event="end", data=None), # ty: ignore
|
||||
StreamPart(event="values", data={"step": 1}, id="1"),
|
||||
StreamPart(event="values", data={"step": 2}, id="2"),
|
||||
StreamPart(event="end", data=None, id="2"),
|
||||
]
|
||||
|
||||
|
||||
@@ -222,9 +222,9 @@ async def test_http_client_stream_recovers_after_disconnect():
|
||||
|
||||
assert call_count == 2
|
||||
assert parts == [
|
||||
StreamPart(event="values", data={"step": 1}),
|
||||
StreamPart(event="values", data={"step": 2}),
|
||||
StreamPart(event="end", data=None),
|
||||
StreamPart(event="values", data={"step": 1}, id="1"),
|
||||
StreamPart(event="values", data={"step": 2}, id="2"),
|
||||
StreamPart(event="end", data=None, id="2"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting OSS Vulnerabilities
|
||||
|
||||
LangChain is partnered with [huntr by Protect AI](https://huntr.com/) to provide
|
||||
a bounty program for our open source projects.
|
||||
|
||||
Please report security vulnerabilities associated with the LangChain
|
||||
open source projects by visiting the following link:
|
||||
|
||||
[https://huntr.com/bounties/disclose/](https://huntr.com/bounties/disclose/?target=https%3A%2F%2Fgithub.com%2Flangchain-ai%2Flangchain&validSearch=true)
|
||||
|
||||
Before reporting a vulnerability, please review:
|
||||
|
||||
1) In-Scope Targets and Out-of-Scope Targets below.
|
||||
2) The [langchain-ai/langchain](https://github.com/langchain-ai/langchain) monorepo structure.
|
||||
3) LangChain [security guidelines](https://python.langchain.com/docs/security) to
|
||||
understand what we consider to be a security vulnerability vs. developer
|
||||
responsibility.
|
||||
|
||||
### In-Scope Targets
|
||||
|
||||
The following packages and repositories are eligible for bug bounties:
|
||||
|
||||
- langchain-core
|
||||
- langchain (see exceptions)
|
||||
- langchain-community (see exceptions)
|
||||
- langgraph
|
||||
- langserve
|
||||
|
||||
### Out of Scope Targets
|
||||
|
||||
All out of scope targets defined by huntr as well as:
|
||||
|
||||
- **langchain-experimental**: This repository is for experimental code and is not
|
||||
eligible for bug bounties, bug reports to it will be marked as interesting or waste of
|
||||
time and published with no bounty attached.
|
||||
- **tools**: Tools in either langchain or langchain-community are not eligible for bug
|
||||
bounties. This includes the following directories
|
||||
- langchain/tools
|
||||
- langchain-community/tools
|
||||
- Please review our [security guidelines](https://python.langchain.com/docs/security)
|
||||
for more details, but generally tools interact with the real world. Developers are
|
||||
expected to understand the security implications of their code and are responsible
|
||||
for the security of their tools.
|
||||
- Code documented with security notices. This will be decided done on a case by
|
||||
case basis, but likely will not be eligible for a bounty as the code is already
|
||||
documented with guidelines for developers that should be followed for making their
|
||||
application secure.
|
||||
- Any LangSmith related repositories or APIs see below.
|
||||
|
||||
## Reporting LangSmith Vulnerabilities
|
||||
|
||||
Please report security vulnerabilities associated with LangSmith by email to `security@langchain.dev`.
|
||||
|
||||
- LangSmith site: <https://smith.langchain.com>
|
||||
- SDK client: <https://github.com/langchain-ai/langsmith-sdk>
|
||||
|
||||
### Other Security Concerns
|
||||
|
||||
For any other security concerns, please contact us at `security@langchain.dev`.
|
||||
Reference in New Issue
Block a user