mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-18 13:45:44 +02:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18a2a2a832 | ||
|
|
5ae0aff522 | ||
|
|
b7d068677c | ||
|
|
706f3e981e | ||
|
|
344ab65351 |
@@ -1,6 +0,0 @@
|
||||
# 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).
|
||||
@@ -116,13 +116,13 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.13"
|
||||
- "3.11"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: "3.13"
|
||||
python-version: "3.11"
|
||||
enable-cache: true
|
||||
cache-suffix: "schema-check-cli"
|
||||
- name: Install CLI dependencies
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
# AGENTS Instructions
|
||||
|
||||
This repository is a monorepo. Each library lives in a subdirectory under `libs/`.
|
||||
|
||||
When you modify code in any library, run the following commands in that library's directory before creating a pull request:
|
||||
|
||||
- `make format` – run code formatters
|
||||
- `make lint` – run the linter
|
||||
- `make test` – execute the test suite
|
||||
|
||||
To run a particular test file or to pass additional pytest options you can specify the `TEST` variable:
|
||||
|
||||
```
|
||||
TEST=path/to/test.py make test
|
||||
```
|
||||
|
||||
Other pytest arguments can also be supplied inside the `TEST` variable.
|
||||
|
||||
## Libraries
|
||||
|
||||
The repository contains several Python and JavaScript/TypeScript libraries.
|
||||
Below is a high-level overview:
|
||||
|
||||
- **checkpoint** – base interfaces for LangGraph checkpointers.
|
||||
- **checkpoint-postgres** – Postgres implementation of the checkpoint saver.
|
||||
- **checkpoint-sqlite** – SQLite implementation of the checkpoint saver.
|
||||
- **cli** – official command-line interface for LangGraph.
|
||||
- **langgraph** – core framework for building stateful, multi-actor agents.
|
||||
- **prebuilt** – high-level APIs for creating and running agents and tools.
|
||||
- **sdk-js** – JS/TS SDK for interacting with the LangGraph REST API.
|
||||
- **sdk-py** – Python SDK for the LangGraph 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
@@ -0,0 +1,293 @@
|
||||
# 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://docs.langchain.com/oss/python/langgraph/overview)
|
||||
[](https://langchain-ai.github.io/langgraph/)
|
||||
|
||||
Trusted by companies shaping the future of agents – including Klarna, Replit, Elastic, and more – LangGraph is a low-level orchestration framework for building, managing, and deploying long-running, stateful agents.
|
||||
|
||||
@@ -23,55 +23,47 @@ Install LangGraph:
|
||||
pip install -U langgraph
|
||||
```
|
||||
|
||||
Create a simple workflow:
|
||||
Then, create an agent [using prebuilt components](https://langchain-ai.github.io/langgraph/agents/agents/):
|
||||
|
||||
```python
|
||||
from langgraph.graph import START, StateGraph
|
||||
from typing_extensions import TypedDict
|
||||
# pip install -qU "langchain[anthropic]" to call the model
|
||||
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
class State(TypedDict):
|
||||
text: str
|
||||
def get_weather(city: str) -> str:
|
||||
"""Get weather for a given city."""
|
||||
return f"It's always sunny in {city}!"
|
||||
|
||||
agent = create_react_agent(
|
||||
model="anthropic:claude-3-7-sonnet-latest",
|
||||
tools=[get_weather],
|
||||
prompt="You are a helpful assistant"
|
||||
)
|
||||
|
||||
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'}
|
||||
# Run the agent
|
||||
agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]}
|
||||
)
|
||||
```
|
||||
|
||||
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).
|
||||
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/).
|
||||
|
||||
## Core benefits
|
||||
|
||||
LangGraph provides low-level supporting infrastructure for *any* long-running, stateful workflow or agent. LangGraph does not abstract prompts or architecture, and provides the following central benefits:
|
||||
|
||||
- [Durable execution](https://docs.langchain.com/oss/python/langgraph/durable-execution): Build agents that persist through failures and can run for extended periods, automatically resuming from exactly where they left off.
|
||||
- [Human-in-the-loop](https://docs.langchain.com/oss/python/langgraph/interrupts): Seamlessly incorporate human oversight by inspecting and modifying agent state at any point during execution.
|
||||
- [Comprehensive memory](https://docs.langchain.com/oss/python/langgraph/memory): Create truly stateful agents with both short-term working memory for ongoing reasoning and long-term persistent memory across sessions.
|
||||
- [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.
|
||||
- [Debugging with LangSmith](http://www.langchain.com/langsmith): Gain deep visibility into complex agent behavior with visualization tools that trace execution paths, capture state transitions, and provide detailed runtime metrics.
|
||||
- [Production-ready deployment](https://docs.langchain.com/langsmith/app-development): Deploy sophisticated agent systems confidently with scalable infrastructure designed to handle the unique challenges of stateful, long-running workflows.
|
||||
- [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.
|
||||
|
||||
## LangGraph’s ecosystem
|
||||
|
||||
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
|
||||
|
||||
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
|
||||
- [LangSmith Deployment](https://docs.langchain.com/langsmith/deployments) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://docs.langchain.com/oss/python/langgraph/studio).
|
||||
- [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/).
|
||||
- [LangChain](https://docs.langchain.com/oss/python/langchain/overview) – Provides integrations and composable components to streamline LLM application development.
|
||||
|
||||
> [!NOTE]
|
||||
@@ -79,11 +71,12 @@ While LangGraph can be used standalone, it also integrates seamlessly with any L
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [Guides](https://docs.langchain.com/oss/python/langgraph/guides): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
|
||||
- [Reference](https://reference.langchain.com/python/langgraph/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
|
||||
- [Examples](https://docs.langchain.com/oss/python/langgraph/agentic-rag): Guided examples on getting started with LangGraph.
|
||||
- [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.
|
||||
- [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,13 +143,11 @@ class PostgresSaver(BasePostgresSaver):
|
||||
"""
|
||||
where, args = self._search_where(config, filter, before)
|
||||
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
|
||||
params = list(args)
|
||||
if limit is not None:
|
||||
query += " LIMIT %s"
|
||||
params.append(int(limit))
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
# if we change this to use .stream() we need to make sure to close the cursor
|
||||
with self._cursor() as cur:
|
||||
cur.execute(query, params)
|
||||
cur.execute(query, args)
|
||||
values = cur.fetchall()
|
||||
if not values:
|
||||
return
|
||||
|
||||
@@ -132,13 +132,11 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
"""
|
||||
where, args = self._search_where(config, filter, before)
|
||||
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
|
||||
params = list(args)
|
||||
if limit is not None:
|
||||
query += " LIMIT %s"
|
||||
params.append(int(limit))
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
# if we change this to use .stream() we need to make sure to close the cursor
|
||||
async with self._cursor() as cur:
|
||||
await cur.execute(query, params, binary=True)
|
||||
await cur.execute(query, args, binary=True)
|
||||
values = await cur.fetchall()
|
||||
if not values:
|
||||
return
|
||||
|
||||
@@ -272,12 +272,10 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
||||
"""
|
||||
where, args = self._search_where(config, filter, before)
|
||||
query = self.SELECT_SQL + where
|
||||
params = list(args)
|
||||
if limit is not None:
|
||||
query += " LIMIT %s"
|
||||
params.append(int(limit))
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
with self._cursor() as cur:
|
||||
cur.execute(query, params, binary=True)
|
||||
cur.execute(self.SELECT_SQL + where, args, binary=True)
|
||||
for value in cur:
|
||||
checkpoint: Checkpoint = {
|
||||
**value["checkpoint"],
|
||||
@@ -638,12 +636,10 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
"""
|
||||
where, args = self._search_where(config, filter, before)
|
||||
query = self.SELECT_SQL + where
|
||||
params = list(args)
|
||||
if limit is not None:
|
||||
query += " LIMIT %s"
|
||||
params.append(int(limit))
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
async with self._cursor() as cur:
|
||||
await cur.execute(query, params, binary=True)
|
||||
await cur.execute(self.SELECT_SQL + where, args, binary=True)
|
||||
async for value in cur:
|
||||
checkpoint: Checkpoint = {
|
||||
**value["checkpoint"],
|
||||
|
||||
@@ -266,27 +266,6 @@ 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,36 +327,31 @@ class BasePostgresStore(Generic[C]):
|
||||
embedding_request: tuple[str, Sequence[tuple[str, str, str, str]]] | None = None
|
||||
if inserts:
|
||||
values = []
|
||||
insertion_params: list[Any] = []
|
||||
insertion_params = []
|
||||
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:
|
||||
@@ -470,10 +465,6 @@ 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)[
|
||||
@@ -1131,27 +1122,6 @@ 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,))
|
||||
@@ -1205,44 +1175,15 @@ def _get_vector_type_ops(store: BasePostgresStore) -> str:
|
||||
|
||||
|
||||
def _get_index_params(store: Any) -> tuple[str, dict[str, Any]]:
|
||||
"""Get a sanitized index type and configuration based on config.
|
||||
|
||||
Only allow known-safe kinds and integer parameters to avoid SQL injection
|
||||
when constructing DDL strings for index creation.
|
||||
"""
|
||||
"""Get the index type and configuration based on config."""
|
||||
if not store.index_config:
|
||||
return "hnsw", {}
|
||||
|
||||
config = cast(PostgresIndexConfig, store.index_config)
|
||||
raw = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
|
||||
|
||||
kind = str(raw.pop("kind", "hnsw"))
|
||||
if kind not in ("hnsw", "ivfflat", "flat"):
|
||||
raise ValueError(
|
||||
f"Invalid index kind for pgvector: {kind}. Expected 'hnsw', 'ivfflat', or 'flat'."
|
||||
)
|
||||
|
||||
raw.pop("vector_type", None)
|
||||
|
||||
if kind == "hnsw":
|
||||
allowed_keys = {"m", "ef_construction"}
|
||||
else: # ivfflat/flat
|
||||
allowed_keys = {"lists", "nlist"}
|
||||
|
||||
sanitized: dict[str, int] = {}
|
||||
for k, v in list(raw.items()):
|
||||
if k not in allowed_keys:
|
||||
continue
|
||||
key = "lists" if k == "nlist" else k
|
||||
try:
|
||||
ivalue = int(v) # type: ignore[call-overload]
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid index parameter value for {k}: {v}") from e
|
||||
if ivalue <= 0:
|
||||
continue
|
||||
sanitized[key] = ivalue
|
||||
|
||||
return kind, sanitized
|
||||
index_config = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
|
||||
kind = index_config.pop("kind", "hnsw")
|
||||
index_config.pop("vector_type", None)
|
||||
return kind, index_config
|
||||
|
||||
|
||||
def _namespace_to_text(
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "3.0.2"
|
||||
version = "3.0.1"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Generated
+416
-564
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,6 @@ 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.
|
||||
@@ -22,7 +20,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 _FILTER_PATTERN.match(key):
|
||||
if not re.match(r"^[a-zA-Z0-9_.-]+$", key):
|
||||
raise ValueError(
|
||||
f"Invalid filter key: '{key}'. Filter keys must contain only alphanumeric characters, underscores, dots, and hyphens."
|
||||
)
|
||||
|
||||
@@ -107,9 +107,6 @@ 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.
|
||||
|
||||
@@ -121,7 +118,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 _FILTER_PATTERN.match(key):
|
||||
if not re.match(r"^[a-zA-Z0-9_.-]+$", key):
|
||||
raise ValueError(
|
||||
f"Invalid filter key: '{key}'. Filter keys must contain only alphanumeric characters, underscores, dots, and hyphens."
|
||||
)
|
||||
@@ -775,9 +772,8 @@ 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,
|
||||
):
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "3.0.1"
|
||||
version = "3.0.0"
|
||||
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -271,39 +271,31 @@ class TestSqliteSaver:
|
||||
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.
|
||||
# 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",
|
||||
]
|
||||
|
||||
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, {})
|
||||
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 = list(saver.list(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
|
||||
|
||||
# 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
|
||||
# Verify the checkpoints table still exists and has all data
|
||||
# (would have been dropped if injection succeeded)
|
||||
results = list(saver.list(None, limit=None))
|
||||
assert len(results) == 5
|
||||
|
||||
@@ -1173,37 +1173,6 @@ def test_boolean_filter_safety(store: SqliteStore) -> None:
|
||||
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 = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
@@ -293,7 +293,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "3.0.1"
|
||||
version = "3.0.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
.langgraph_api/
|
||||
+1
-4
@@ -1,4 +1,4 @@
|
||||
.PHONY: test lint format test-integration update-schema bump-version
|
||||
.PHONY: test lint format test-integration update-schema
|
||||
|
||||
######################
|
||||
# TESTING AND COVERAGE
|
||||
@@ -35,6 +35,3 @@ format format_diff:
|
||||
|
||||
update-schema:
|
||||
uv run python generate_schema.py
|
||||
|
||||
bump-version:
|
||||
uv run hatch version patch
|
||||
|
||||
@@ -27,8 +27,6 @@ from langgraph_cli.schemas import (
|
||||
StoreConfig,
|
||||
ThreadTTLConfig,
|
||||
TTLConfig,
|
||||
WebhooksConfig,
|
||||
WebhookUrlPolicy,
|
||||
)
|
||||
|
||||
|
||||
@@ -120,8 +118,6 @@ def add_descriptions_to_schema(schema, cls):
|
||||
SerdeConfig,
|
||||
TTLConfig,
|
||||
ConfigurableHeaderConfig,
|
||||
WebhooksConfig,
|
||||
WebhookUrlPolicy,
|
||||
]:
|
||||
if potential_cls.__name__ == def_name:
|
||||
add_descriptions_to_schema(def_schema, potential_cls)
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.4.10"
|
||||
__version__ = "0.4.7"
|
||||
|
||||
@@ -336,13 +336,13 @@ def _build(
|
||||
|
||||
# apply config
|
||||
stdin, additional_contexts = langgraph_cli.config.config_to_docker(
|
||||
config_path=config,
|
||||
config=config_json,
|
||||
base_image=base_image,
|
||||
api_version=api_version,
|
||||
install_command=install_command,
|
||||
build_command=build_command,
|
||||
build_context=build_context,
|
||||
config,
|
||||
config_json,
|
||||
base_image,
|
||||
api_version,
|
||||
install_command,
|
||||
build_command,
|
||||
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_path=config,
|
||||
config=config_json,
|
||||
config,
|
||||
config_json,
|
||||
base_image=base_image,
|
||||
api_version=api_version,
|
||||
)
|
||||
@@ -760,7 +760,6 @@ 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,
|
||||
|
||||
@@ -156,8 +156,6 @@ def validate_config(config: Config) -> Config:
|
||||
"auth": config.get("auth"),
|
||||
"encryption": config.get("encryption"),
|
||||
"http": config.get("http"),
|
||||
# Pass through webhooks config so it can be injected into the image
|
||||
"webhooks": config.get("webhooks"),
|
||||
"checkpointer": config.get("checkpointer"),
|
||||
"ui": config.get("ui"),
|
||||
"ui_config": config.get("ui_config"),
|
||||
@@ -824,8 +822,6 @@ 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")
|
||||
@@ -963,10 +959,6 @@ ADD {relpath} /deps/{name}
|
||||
if (http_config := config.get("http")) is not None:
|
||||
env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
|
||||
|
||||
# Inject webhooks configuration if provided
|
||||
if (webhooks_config := config.get("webhooks")) is not None:
|
||||
env_vars.append(f"ENV LANGGRAPH_WEBHOOKS='{json.dumps(webhooks_config)}'")
|
||||
|
||||
if (checkpointer_config := config.get("checkpointer")) is not None:
|
||||
env_vars.append(
|
||||
f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'"
|
||||
@@ -1005,7 +997,6 @@ ADD {relpath} /deps/{name}
|
||||
)
|
||||
|
||||
# Add main dockerfile content
|
||||
dep_vname = "$$dep" if escape_variables else "$dep"
|
||||
docker_file_contents.extend(
|
||||
[
|
||||
f"FROM {image_str}",
|
||||
@@ -1016,10 +1007,10 @@ ADD {relpath} /deps/{name}
|
||||
"",
|
||||
"# -- Installing all local dependencies --",
|
||||
f"""RUN for dep in /deps/*; do \
|
||||
echo "Installing {dep_vname}"; \
|
||||
if [ -d "{dep_vname}" ]; then \
|
||||
echo "Installing {dep_vname}"; \
|
||||
(cd "{dep_vname}" && {global_reqs_pip_install} -e .); \
|
||||
echo "Installing $dep"; \
|
||||
if [ -d "$dep" ]; then \
|
||||
echo "Installing $dep"; \
|
||||
(cd "$dep" && {global_reqs_pip_install} -e .); \
|
||||
fi; \
|
||||
done""",
|
||||
"# -- End of local dependencies install --",
|
||||
@@ -1094,10 +1085,6 @@ def node_config_to_docker(
|
||||
if (http_config := config.get("http")) is not None:
|
||||
env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
|
||||
|
||||
# Inject webhooks configuration if provided
|
||||
if (webhooks_config := config.get("webhooks")) is not None:
|
||||
env_vars.append(f"ENV LANGGRAPH_WEBHOOKS='{json.dumps(webhooks_config)}'")
|
||||
|
||||
if (checkpointer_config := config.get("checkpointer")) is not None:
|
||||
env_vars.append(
|
||||
f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'"
|
||||
@@ -1205,34 +1192,26 @@ 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_path,
|
||||
config=config,
|
||||
base_image=base_image,
|
||||
api_version=api_version,
|
||||
install_command=install_command,
|
||||
build_command=build_command,
|
||||
build_context=build_context,
|
||||
config_path,
|
||||
config,
|
||||
base_image,
|
||||
api_version,
|
||||
install_command,
|
||||
build_command,
|
||||
build_context,
|
||||
)
|
||||
|
||||
return python_config_to_docker(
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
base_image=base_image,
|
||||
api_version=api_version,
|
||||
escape_variables=escape_variables,
|
||||
)
|
||||
return python_config_to_docker(config_path, config, base_image, api_version)
|
||||
|
||||
|
||||
def config_to_compose(
|
||||
@@ -1276,11 +1255,7 @@ def config_to_compose(
|
||||
|
||||
else:
|
||||
dockerfile, additional_contexts = config_to_docker(
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
base_image=base_image,
|
||||
api_version=api_version,
|
||||
escape_variables=True,
|
||||
config_path, config, base_image, api_version
|
||||
)
|
||||
|
||||
additional_contexts_str = "\n".join(
|
||||
|
||||
@@ -362,7 +362,7 @@ class CorsConfig(TypedDict, total=False):
|
||||
"""
|
||||
|
||||
|
||||
class ConfigurableHeaderConfig(TypedDict, total=False):
|
||||
class ConfigurableHeaderConfig(TypedDict):
|
||||
"""Customize which headers to include as configurable values in your runs.
|
||||
|
||||
By default, omits x-api-key, x-tenant-id, and x-service-key.
|
||||
@@ -373,7 +373,7 @@ class ConfigurableHeaderConfig(TypedDict, total=False):
|
||||
"""
|
||||
|
||||
includes: list[str] | None
|
||||
"""Headers to include (if not also matched against an 'excludes' pattern).
|
||||
"""Headers to include (if not also matches against an 'exludes' pattern.
|
||||
|
||||
Examples:
|
||||
- 'user-agent'
|
||||
@@ -485,46 +485,6 @@ class HttpConfig(TypedDict, total=False):
|
||||
"""
|
||||
|
||||
|
||||
class WebhookUrlPolicy(TypedDict, total=False):
|
||||
require_https: bool
|
||||
"""Enforce HTTPS scheme for absolute URLs; reject `http://` when true."""
|
||||
allowed_domains: list[str]
|
||||
"""Hostname allowlist. Supports exact hosts and wildcard subdomains.
|
||||
|
||||
Use entries like "hooks.example.com" or "*.mycorp.com". The wildcard only
|
||||
matches subdomains ("foo.mycorp.com"), not the apex ("mycorp.com"). When
|
||||
empty or omitted, any public host is allowed (subject to SSRF IP checks).
|
||||
"""
|
||||
allowed_ports: list[int]
|
||||
"""Explicit port allowlist for absolute URLs.
|
||||
|
||||
If set, requests must use one of these ports. Defaults are respected when
|
||||
a port is not present in the URL (443 for https, 80 for http).
|
||||
"""
|
||||
max_url_length: int
|
||||
"""Maximum permitted URL length in characters; longer inputs are rejected early."""
|
||||
disable_loopback: bool
|
||||
"""Disallow relative URLs (internal loopback calls) when true."""
|
||||
|
||||
|
||||
class WebhooksConfig(TypedDict, total=False):
|
||||
env_prefix: str
|
||||
"""Required prefix for environment variables referenced in header templates.
|
||||
|
||||
Acts as an allowlist boundary to prevent leaking arbitrary environment
|
||||
variables. Defaults to "LG_WEBHOOK_" when omitted.
|
||||
"""
|
||||
url: WebhookUrlPolicy
|
||||
"""URL validation policy for user-supplied webhook endpoints."""
|
||||
headers: dict[str, str]
|
||||
"""Static headers to include with webhook requests.
|
||||
|
||||
Values may contain templates of the form "${{ env.VAR }}". On startup, these
|
||||
are resolved via the process environment after verifying `VAR` starts with
|
||||
`env_prefix`. Mixed literals and multiple templates are allowed.
|
||||
"""
|
||||
|
||||
|
||||
class Config(TypedDict, total=False):
|
||||
"""Top-level config for langgraph-cli or similar deployment tooling."""
|
||||
|
||||
@@ -653,13 +613,6 @@ class Config(TypedDict, total=False):
|
||||
and how cross-origin requests are handled.
|
||||
"""
|
||||
|
||||
webhooks: WebhooksConfig | None
|
||||
"""Optional. Webhooks configuration for outbound event delivery.
|
||||
|
||||
Forwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`
|
||||
for URL policy and header templating details.
|
||||
"""
|
||||
|
||||
ui: dict[str, str] | None
|
||||
"""Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.
|
||||
"""
|
||||
|
||||
@@ -19,7 +19,7 @@ dependencies = [
|
||||
path = "langgraph_cli/__init__.py"
|
||||
[project.optional-dependencies]
|
||||
inmem = [
|
||||
"langgraph-api>=0.5.35,<0.6.0 ; python_version >= '3.11'",
|
||||
"langgraph-api>=0.4,<0.6.0 ; python_version >= '3.11'",
|
||||
"langgraph-runtime-inmem>=0.7 ; python_version >= '3.11'",
|
||||
"python-dotenv>=0.8.0",
|
||||
]
|
||||
@@ -49,7 +49,6 @@ lint = [
|
||||
dev = [
|
||||
{include-group = "test"},
|
||||
{include-group = "lint"},
|
||||
"hatch>=1.16.2",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
|
||||
@@ -210,17 +210,6 @@
|
||||
}
|
||||
],
|
||||
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
|
||||
},
|
||||
"webhooks": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/WebhooksConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -424,17 +413,6 @@
|
||||
}
|
||||
],
|
||||
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
|
||||
},
|
||||
"webhooks": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/WebhooksConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -638,7 +616,7 @@
|
||||
},
|
||||
"EncryptionConfig": {
|
||||
"title": "EncryptionConfig",
|
||||
"description": "Configuration for custom at-rest encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database,\nincluding metadata fields and checkpoint blobs.",
|
||||
"description": "Configuration for custom at-rest encryption logic.\n\n Allows you to implement custom encryption for sensitive data stored in the database,\n including metadata fields and checkpoint blobs.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
@@ -781,10 +759,13 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Headers to include (if not also matched against an 'excludes' pattern).\n"
|
||||
"description": "Headers to include (if not also matches against an 'exludes' pattern.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
"required": [
|
||||
"excludes",
|
||||
"includes"
|
||||
]
|
||||
},
|
||||
"CorsConfig": {
|
||||
"title": "CorsConfig",
|
||||
@@ -927,63 +908,6 @@
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"WebhooksConfig": {
|
||||
"title": "WebhooksConfig",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env_prefix": {
|
||||
"type": "string",
|
||||
"description": "Required prefix for environment variables referenced in header templates.\n\nActs as an allowlist boundary to prevent leaking arbitrary environment\nvariables. Defaults to \"LG_WEBHOOK_\" when omitted.\n"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Static headers to include with webhook requests.\n\nValues may contain templates of the form \"${{ env.VAR }}\". On startup, these\nare resolved via the process environment after verifying `VAR` starts with\n`env_prefix`. Mixed literals and multiple templates are allowed.\n"
|
||||
},
|
||||
"url": {
|
||||
"$ref": "#/$defs/WebhookUrlPolicy",
|
||||
"description": "URL validation policy for user-supplied webhook endpoints."
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
|
||||
},
|
||||
"WebhookUrlPolicy": {
|
||||
"title": "WebhookUrlPolicy",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allowed_domains": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Hostname allowlist. Supports exact hosts and wildcard subdomains.\n\nUse entries like \"hooks.example.com\" or \"*.mycorp.com\". The wildcard only\nmatches subdomains (\"foo.mycorp.com\"), not the apex (\"mycorp.com\"). When\nempty or omitted, any public host is allowed (subject to SSRF IP checks).\n"
|
||||
},
|
||||
"allowed_ports": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": "Explicit port allowlist for absolute URLs.\n\nIf set, requests must use one of these ports. Defaults are respected when\na port is not present in the URL (443 for https, 80 for http).\n"
|
||||
},
|
||||
"disable_loopback": {
|
||||
"type": "boolean",
|
||||
"description": "Disallow relative URLs (internal loopback calls) when true."
|
||||
},
|
||||
"max_url_length": {
|
||||
"type": "integer",
|
||||
"description": "Maximum permitted URL length in characters; longer inputs are rejected early."
|
||||
},
|
||||
"require_https": {
|
||||
"type": "boolean",
|
||||
"description": "Enforce HTTPS scheme for absolute URLs; reject `http://` when true."
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
|
||||
}
|
||||
},
|
||||
"title": "LangGraph CLI Configuration",
|
||||
|
||||
@@ -210,17 +210,6 @@
|
||||
}
|
||||
],
|
||||
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
|
||||
},
|
||||
"webhooks": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/WebhooksConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -424,17 +413,6 @@
|
||||
}
|
||||
],
|
||||
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
|
||||
},
|
||||
"webhooks": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/WebhooksConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -638,7 +616,7 @@
|
||||
},
|
||||
"EncryptionConfig": {
|
||||
"title": "EncryptionConfig",
|
||||
"description": "Configuration for custom at-rest encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database,\nincluding metadata fields and checkpoint blobs.",
|
||||
"description": "Configuration for custom at-rest encryption logic.\n\n Allows you to implement custom encryption for sensitive data stored in the database,\n including metadata fields and checkpoint blobs.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
@@ -781,10 +759,13 @@
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Headers to include (if not also matched against an 'excludes' pattern).\n"
|
||||
"description": "Headers to include (if not also matches against an 'exludes' pattern.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
"required": [
|
||||
"excludes",
|
||||
"includes"
|
||||
]
|
||||
},
|
||||
"CorsConfig": {
|
||||
"title": "CorsConfig",
|
||||
@@ -927,63 +908,6 @@
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"WebhooksConfig": {
|
||||
"title": "WebhooksConfig",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env_prefix": {
|
||||
"type": "string",
|
||||
"description": "Required prefix for environment variables referenced in header templates.\n\nActs as an allowlist boundary to prevent leaking arbitrary environment\nvariables. Defaults to \"LG_WEBHOOK_\" when omitted.\n"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Static headers to include with webhook requests.\n\nValues may contain templates of the form \"${{ env.VAR }}\". On startup, these\nare resolved via the process environment after verifying `VAR` starts with\n`env_prefix`. Mixed literals and multiple templates are allowed.\n"
|
||||
},
|
||||
"url": {
|
||||
"$ref": "#/$defs/WebhookUrlPolicy",
|
||||
"description": "URL validation policy for user-supplied webhook endpoints."
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
|
||||
},
|
||||
"WebhookUrlPolicy": {
|
||||
"title": "WebhookUrlPolicy",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allowed_domains": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Hostname allowlist. Supports exact hosts and wildcard subdomains.\n\nUse entries like \"hooks.example.com\" or \"*.mycorp.com\". The wildcard only\nmatches subdomains (\"foo.mycorp.com\"), not the apex (\"mycorp.com\"). When\nempty or omitted, any public host is allowed (subject to SSRF IP checks).\n"
|
||||
},
|
||||
"allowed_ports": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": "Explicit port allowlist for absolute URLs.\n\nIf set, requests must use one of these ports. Defaults are respected when\na port is not present in the URL (443 for https, 80 for http).\n"
|
||||
},
|
||||
"disable_loopback": {
|
||||
"type": "boolean",
|
||||
"description": "Disallow relative URLs (internal loopback calls) when true."
|
||||
},
|
||||
"max_url_length": {
|
||||
"type": "integer",
|
||||
"description": "Maximum permitted URL length in characters; longer inputs are rejected early."
|
||||
},
|
||||
"require_https": {
|
||||
"type": "boolean",
|
||||
"description": "Enforce HTTPS scheme for absolute URLs; reject `http://` when true."
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
|
||||
}
|
||||
},
|
||||
"title": "LangGraph CLI Configuration",
|
||||
|
||||
@@ -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), " ")}
|
||||
|
||||
@@ -49,7 +49,6 @@ def test_validate_config():
|
||||
"store": None,
|
||||
"auth": None,
|
||||
"encryption": None,
|
||||
"webhooks": None,
|
||||
"checkpointer": None,
|
||||
"http": None,
|
||||
"ui": None,
|
||||
@@ -77,7 +76,6 @@ def test_validate_config():
|
||||
"store": None,
|
||||
"auth": None,
|
||||
"encryption": None,
|
||||
"webhooks": None,
|
||||
"checkpointer": None,
|
||||
"http": None,
|
||||
"ui": None,
|
||||
@@ -420,7 +418,7 @@ def test_config_to_docker_simple():
|
||||
"http": {"app": "../../examples/my_app.py:app"},
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraph-api",
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = f"""\
|
||||
# syntax=docker/dockerfile:1.4
|
||||
@@ -483,7 +481,7 @@ def test_config_to_docker_outside_path():
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"dependencies": [".", ".."], "graphs": graphs}),
|
||||
base_image="langchain/langgraph-api",
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = (
|
||||
"""\
|
||||
@@ -544,7 +542,7 @@ def test_config_to_docker_pipconfig():
|
||||
"pip_config_file": "pipconfig.txt",
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraph-api",
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = (
|
||||
"""\
|
||||
@@ -585,7 +583,7 @@ def test_config_to_docker_invalid_inputs():
|
||||
config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"dependencies": ["./missing"], "graphs": graphs}),
|
||||
base_image="langchain/langgraph-api",
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
|
||||
# test missing local module
|
||||
@@ -594,7 +592,7 @@ def test_config_to_docker_invalid_inputs():
|
||||
config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"dependencies": ["."], "graphs": graphs}),
|
||||
base_image="langchain/langgraph-api",
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
|
||||
|
||||
@@ -608,7 +606,7 @@ def test_config_to_docker_local_deps():
|
||||
"graphs": graphs,
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraph-api-custom",
|
||||
"langchain/langgraph-api-custom",
|
||||
)
|
||||
expected_docker_stdin = f"""\
|
||||
FROM langchain/langgraph-api-custom:3.11
|
||||
@@ -654,7 +652,7 @@ dependencies = ["langchain"]"""
|
||||
"graphs": graphs,
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraph-api",
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
os.remove(pyproject_path)
|
||||
expected_docker_stdin = (
|
||||
@@ -689,7 +687,7 @@ def test_config_to_docker_end_to_end():
|
||||
"dockerfile_lines": ["ARG meow", "ARG foo"],
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraph-api",
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = f"""FROM langchain/langgraph-api:3.12
|
||||
ARG meow
|
||||
@@ -734,7 +732,7 @@ def test_config_to_docker_nodejs():
|
||||
"ui_config": {"shared": ["nuqs"]},
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraphjs-api",
|
||||
"langchain/langgraphjs-api",
|
||||
)
|
||||
expected_docker_stdin = """FROM langchain/langgraphjs-api:20
|
||||
ARG meow
|
||||
@@ -786,7 +784,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, _ = config_to_docker(
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -796,14 +794,11 @@ def test_config_to_docker_python_encryption_formatted():
|
||||
"encryption": {"path": "./agent.py:my_encryption"},
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraph-api",
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
# Verify that LANGGRAPH_ENCRYPTION is in the docker output with the correct path
|
||||
assert "LANGGRAPH_ENCRYPTION=" in actual_docker_stdin
|
||||
assert (
|
||||
"/deps/outer-unit_tests/unit_tests/agent.py:my_encryption"
|
||||
in actual_docker_stdin
|
||||
)
|
||||
assert "/deps/outer-unit_tests/unit_tests/agent.py:my_encryption" in actual_docker_stdin
|
||||
|
||||
|
||||
def test_config_to_docker_nodejs_internal_docker_tag():
|
||||
@@ -821,7 +816,7 @@ def test_config_to_docker_nodejs_internal_docker_tag():
|
||||
"_INTERNAL_docker_tag": "my-tag",
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraphjs-api",
|
||||
"langchain/langgraphjs-api",
|
||||
)
|
||||
expected_docker_stdin = """FROM langchain/langgraphjs-api:my-tag
|
||||
ARG meow
|
||||
@@ -839,85 +834,6 @@ RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not foun
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
def _extract_env_json(dockerfile: str, var_name: str) -> dict:
|
||||
"""Helper to extract and parse a JSON value from an ENV line in a Dockerfile."""
|
||||
line_prefix = f"ENV {var_name}='"
|
||||
for line in dockerfile.splitlines():
|
||||
if line.startswith(line_prefix) and line.endswith("'"):
|
||||
json_str = line[len(line_prefix) : -1]
|
||||
return json.loads(json_str)
|
||||
raise AssertionError(f"{var_name} not found in Dockerfile env lines")
|
||||
|
||||
|
||||
def test_config_to_docker_webhooks_python():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
webhooks = {
|
||||
"env_prefix": "LG_WEBHOOK_",
|
||||
"url": {
|
||||
"require_https": True,
|
||||
"allowed_domains": ["hooks.example.com", "*.example.org"],
|
||||
"allowed_ports": [443],
|
||||
"max_url_length": 1024,
|
||||
"disable_loopback": False,
|
||||
},
|
||||
"headers": {
|
||||
"x-auth": "${{ env.LG_WEBHOOK_TOKEN }}",
|
||||
"x-mixed": "Bearer ${{ env.LG_WEBHOOK_TOKEN }}-suffix",
|
||||
},
|
||||
}
|
||||
|
||||
dockerfile, _ = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": graphs,
|
||||
"webhooks": webhooks,
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
|
||||
# Ensure the ENV line is present and the payload round-trips via JSON
|
||||
parsed = _extract_env_json(dockerfile, "LANGGRAPH_WEBHOOKS")
|
||||
assert parsed == webhooks
|
||||
|
||||
|
||||
def test_config_to_docker_webhooks_node():
|
||||
graphs = {"agent": "./graphs/agent.js:graph"}
|
||||
webhooks = {
|
||||
"env_prefix": "LG_WEBHOOK_",
|
||||
"url": {"require_https": True},
|
||||
"headers": {"x-auth": "${{ env.LG_WEBHOOK_TOKEN }}"},
|
||||
}
|
||||
|
||||
dockerfile, _ = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
"node_version": "20",
|
||||
"graphs": graphs,
|
||||
"webhooks": webhooks,
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraphjs-api",
|
||||
)
|
||||
|
||||
parsed = _extract_env_json(dockerfile, "LANGGRAPH_WEBHOOKS")
|
||||
assert parsed == webhooks
|
||||
|
||||
|
||||
def test_config_to_docker_no_webhooks():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
dockerfile, _ = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"dependencies": ["."], "graphs": graphs}),
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
|
||||
assert "ENV LANGGRAPH_WEBHOOKS=" not in dockerfile
|
||||
|
||||
|
||||
def test_config_to_docker_gen_ui_python():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
@@ -930,7 +846,7 @@ def test_config_to_docker_gen_ui_python():
|
||||
"ui_config": {"shared": ["nuqs"]},
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraph-api",
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
|
||||
expected_docker_stdin = f"""FROM langchain/langgraph-api:3.11
|
||||
@@ -976,7 +892,7 @@ def test_config_to_docker_multiplatform():
|
||||
validate_config(
|
||||
{"node_version": "22", "dependencies": ["."], "graphs": graphs}
|
||||
),
|
||||
base_image="langchain/langgraph-api",
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
|
||||
expected_docker_stdin = f"""FROM langchain/langgraph-api:3.11
|
||||
@@ -1024,7 +940,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, base_image="langchain/langgraph-api:0.2.47"
|
||||
PATH_TO_CONFIG, config_auto, "langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert "uv pip install --system " in docker_auto
|
||||
assert "rm /usr/bin/uv /usr/bin/uvx" in docker_auto
|
||||
@@ -1032,7 +948,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, base_image="langchain/langgraph-api:0.2.47"
|
||||
PATH_TO_CONFIG, config_pip, "langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert "uv pip install --system " not in docker_pip
|
||||
assert "pip install" in docker_pip
|
||||
@@ -1041,7 +957,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, base_image="langchain/langgraph-api:0.2.47"
|
||||
PATH_TO_CONFIG, config_uv, "langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert "uv pip install --system " in docker_uv
|
||||
assert "rm /usr/bin/uv /usr/bin/uvx" in docker_uv
|
||||
@@ -1051,7 +967,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, base_image="langchain/langgraph-api:0.2.46"
|
||||
PATH_TO_CONFIG, config_auto_old, "langchain/langgraph-api:0.2.46"
|
||||
)
|
||||
assert "uv pip install --system " not in docker_auto_old
|
||||
assert "pip install" in docker_auto_old
|
||||
@@ -1060,7 +976,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, base_image="langchain/langgraph-api:0.2.47"
|
||||
PATH_TO_CONFIG, config_default, "langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert "uv pip install --system " in docker_default
|
||||
|
||||
@@ -1076,7 +992,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, base_image="langchain/langgraph-api:0.2.47"
|
||||
PATH_TO_CONFIG, config_true, "langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert not any(
|
||||
"/usr/local/lib/python*/site-packages/" + pckg + "*" in docker_true
|
||||
@@ -1087,7 +1003,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, base_image="langchain/langgraph-api:0.2.47"
|
||||
PATH_TO_CONFIG, config_false, "langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert all(
|
||||
"/usr/local/lib/python*/site-packages/" + pckg + "*" in docker_false
|
||||
@@ -1098,7 +1014,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, base_image="langchain/langgraph-api:0.2.47"
|
||||
PATH_TO_CONFIG, config_list, "langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
assert all(
|
||||
"/usr/local/lib/python*/site-packages/" + pckg + "*" in docker_list
|
||||
@@ -1137,7 +1053,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 +1094,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 +1139,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 +1177,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 +1224,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 +1535,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}),
|
||||
base_image="langchain/langgraph-api",
|
||||
"langchain/langgraph-api",
|
||||
api_version="0.2.74",
|
||||
)
|
||||
|
||||
@@ -1633,7 +1549,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}),
|
||||
base_image="langchain/langgraphjs-api",
|
||||
"langchain/langgraphjs-api",
|
||||
api_version="0.2.74",
|
||||
)
|
||||
|
||||
|
||||
Generated
+410
-977
File diff suppressed because it is too large
Load Diff
+26
-33
@@ -11,7 +11,7 @@
|
||||
[](https://pypi.org/project/langgraph/)
|
||||
[](https://pepy.tech/project/langgraph)
|
||||
[](https://github.com/langchain-ai/langgraph/issues)
|
||||
[](https://docs.langchain.com/oss/python/langgraph/overview)
|
||||
[](https://langchain-ai.github.io/langgraph/)
|
||||
|
||||
Trusted by companies shaping the future of agents – including Klarna, Replit, Elastic, and more – LangGraph is a low-level orchestration framework for building, managing, and deploying long-running, stateful agents.
|
||||
|
||||
@@ -23,55 +23,47 @@ Install LangGraph:
|
||||
pip install -U langgraph
|
||||
```
|
||||
|
||||
Create a simple workflow:
|
||||
Then, create an agent [using prebuilt components](https://langchain-ai.github.io/langgraph/agents/agents/):
|
||||
|
||||
```python
|
||||
from langgraph.graph import START, StateGraph
|
||||
from typing_extensions import TypedDict
|
||||
# pip install -qU "langchain[anthropic]" to call the model
|
||||
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
class State(TypedDict):
|
||||
text: str
|
||||
def get_weather(city: str) -> str:
|
||||
"""Get weather for a given city."""
|
||||
return f"It's always sunny in {city}!"
|
||||
|
||||
agent = create_react_agent(
|
||||
model="anthropic:claude-3-7-sonnet-latest",
|
||||
tools=[get_weather],
|
||||
prompt="You are a helpful assistant"
|
||||
)
|
||||
|
||||
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'}
|
||||
# Run the agent
|
||||
agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]}
|
||||
)
|
||||
```
|
||||
|
||||
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).
|
||||
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/).
|
||||
|
||||
## Core benefits
|
||||
|
||||
LangGraph provides low-level supporting infrastructure for *any* long-running, stateful workflow or agent. LangGraph does not abstract prompts or architecture, and provides the following central benefits:
|
||||
|
||||
- [Durable execution](https://docs.langchain.com/oss/python/langgraph/durable-execution): Build agents that persist through failures and can run for extended periods, automatically resuming from exactly where they left off.
|
||||
- [Human-in-the-loop](https://docs.langchain.com/oss/python/langgraph/interrupts): Seamlessly incorporate human oversight by inspecting and modifying agent state at any point during execution.
|
||||
- [Comprehensive memory](https://docs.langchain.com/oss/python/langgraph/memory): Create truly stateful agents with both short-term working memory for ongoing reasoning and long-term persistent memory across sessions.
|
||||
- [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.
|
||||
- [Debugging with LangSmith](http://www.langchain.com/langsmith): Gain deep visibility into complex agent behavior with visualization tools that trace execution paths, capture state transitions, and provide detailed runtime metrics.
|
||||
- [Production-ready deployment](https://docs.langchain.com/langsmith/app-development): Deploy sophisticated agent systems confidently with scalable infrastructure designed to handle the unique challenges of stateful, long-running workflows.
|
||||
- [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.
|
||||
|
||||
## LangGraph’s ecosystem
|
||||
|
||||
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
|
||||
|
||||
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
|
||||
- [LangSmith Deployment](https://docs.langchain.com/langsmith/deployments) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://docs.langchain.com/oss/python/langgraph/studio).
|
||||
- [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/).
|
||||
- [LangChain](https://docs.langchain.com/oss/python/langchain/overview) – Provides integrations and composable components to streamline LLM application development.
|
||||
|
||||
> [!NOTE]
|
||||
@@ -79,11 +71,12 @@ While LangGraph can be used standalone, it also integrates seamlessly with any L
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [Guides](https://docs.langchain.com/oss/python/langgraph/guides): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
|
||||
- [Reference](https://reference.langchain.com/python/langgraph/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
|
||||
- [Examples](https://docs.langchain.com/oss/python/langgraph/agentic-rag): Guided examples on getting started with LangGraph.
|
||||
- [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.
|
||||
- [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,7 +78,6 @@ 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
|
||||
@@ -854,8 +853,6 @@ 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,7 +142,6 @@ from langgraph.types import (
|
||||
StateSnapshot,
|
||||
StateUpdate,
|
||||
StreamMode,
|
||||
ensure_valid_checkpointer,
|
||||
)
|
||||
from langgraph.typing import ContextT, InputT, OutputT, StateT
|
||||
from langgraph.warnings import LangGraphDeprecatedSinceV10
|
||||
@@ -643,7 +642,7 @@ class Pregel(
|
||||
input_channels: str | Sequence[str],
|
||||
step_timeout: float | None = None,
|
||||
debug: bool | None = None,
|
||||
checkpointer: Checkpointer = None,
|
||||
checkpointer: BaseCheckpointSaver | None = None,
|
||||
store: BaseStore | None = None,
|
||||
cache: BaseCache | None = None,
|
||||
retry_policy: RetryPolicy | Sequence[RetryPolicy] = (),
|
||||
@@ -666,8 +665,6 @@ 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,7 +56,6 @@ __all__ = (
|
||||
"Durability",
|
||||
"interrupt",
|
||||
"Overwrite",
|
||||
"ensure_valid_checkpointer",
|
||||
)
|
||||
|
||||
Durability = Literal["sync", "async", "exit"]
|
||||
@@ -74,20 +73,6 @@ 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.5"
|
||||
version = "1.0.4"
|
||||
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.3.0,<0.4.0",
|
||||
"langgraph-sdk>=0.2.2,<0.3.0",
|
||||
"langgraph-prebuilt>=1.0.2,<1.1.0",
|
||||
"xxhash>=3.5.0",
|
||||
"pydantic>=2.7.4",
|
||||
|
||||
@@ -120,22 +120,6 @@ 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"}, None),
|
||||
("updates", {"chunk": "data4"}, None),
|
||||
("updates", {"__interrupt__": ()}, None),
|
||||
("updates", {"chunk": "data3"}),
|
||||
("updates", {"chunk": "data4"}),
|
||||
("updates", {"__interrupt__": ()}),
|
||||
]
|
||||
|
||||
# subgraphs + list modes
|
||||
@@ -739,9 +739,9 @@ async def test_astream():
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert stream_parts == [
|
||||
("updates", {"chunk": "data3"}, None),
|
||||
("updates", {"chunk": "data4"}, None),
|
||||
("updates", {"__interrupt__": ()}, None),
|
||||
("updates", {"chunk": "data3"}),
|
||||
("updates", {"chunk": "data4"}),
|
||||
("updates", {"__interrupt__": ()}),
|
||||
]
|
||||
|
||||
# subgraphs + list modes
|
||||
|
||||
Generated
+76
-77
@@ -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 python_full_version < '3.14' and implementation_name == 'cpython'" },
|
||||
{ name = "forbiddenfruit", marker = "python_full_version >= '3.11' and implementation_name == 'cpython'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e0/dcbab602790a576b0b94108c07e2c048e5897df7cc83722a89582d733987/blockbuster-1.5.26.tar.gz", hash = "sha256:cc3ce8c70fa852a97ee3411155f31e4ad2665cd1c6c7d2f8bb1851dab61dc629", size = 36085, upload-time = "2025-12-05T10:43:47.735Z" }
|
||||
wheels = [
|
||||
@@ -387,7 +387,7 @@ name = "click"
|
||||
version = "8.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "python_full_version < '3.14' and sys_platform == 'win32'" },
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
||||
wheels = [
|
||||
@@ -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 python_full_version < '3.14' and platform_python_implementation != 'PyPy'" },
|
||||
{ name = "cffi", marker = "python_full_version >= '3.11' 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' and python_full_version < '3.14'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" }
|
||||
wheels = [
|
||||
@@ -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' and python_full_version < '3.14'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
|
||||
wheels = [
|
||||
@@ -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' 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'" },
|
||||
{ name = "grpcio", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "setuptools", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/76/0cd2a2bb379275c319544a3ab613dc3cea7a167503908c1b4de55f82bd9e/grpcio_tools-1.75.1.tar.gz", hash = "sha256:bb78960cf3d58941e1fec70cbdaccf255918beed13c34112a6915a6d8facebd1", size = 5390470, upload-time = "2025-09-26T09:10:11.948Z" }
|
||||
wheels = [
|
||||
@@ -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' and python_full_version < '3.14'" },
|
||||
{ name = "zipp", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
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.5"
|
||||
version = "1.0.4"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -1488,39 +1488,39 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-api"
|
||||
version = "0.5.35"
|
||||
version = "0.5.30"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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'" },
|
||||
{ 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'" },
|
||||
]
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1572,7 +1572,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "3.0.2"
|
||||
version = "3.0.1"
|
||||
source = { editable = "../checkpoint-postgres" }
|
||||
dependencies = [
|
||||
{ name = "langgraph-checkpoint" },
|
||||
@@ -1619,7 +1619,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "3.0.1"
|
||||
version = "3.0.0"
|
||||
source = { editable = "../checkpoint-sqlite" }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
@@ -1664,21 +1664,21 @@ test = [
|
||||
name = "langgraph-cli"
|
||||
source = { editable = "../cli" }
|
||||
dependencies = [
|
||||
{ name = "click", marker = "python_full_version < '3.14'" },
|
||||
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "click" },
|
||||
{ name = "langgraph-sdk", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
inmem = [
|
||||
{ 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'" },
|
||||
{ name = "langgraph-api", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "python-dotenv" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "click", specifier = ">=8.1.7" },
|
||||
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.5.35,<0.6.0" },
|
||||
{ name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.4,<0.6.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,7 +1688,6 @@ provides-extras = ["inmem"]
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "codespell" },
|
||||
{ name = "hatch", specifier = ">=1.16.2" },
|
||||
{ name = "msgspec" },
|
||||
{ name = "mypy" },
|
||||
{ name = "pytest" },
|
||||
@@ -1766,12 +1765,12 @@ name = "langgraph-runtime-inmem"
|
||||
version = "0.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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'" },
|
||||
{ name = "blockbuster", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "langgraph-checkpoint", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sse-starlette", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "starlette", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "structlog", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/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 = [
|
||||
@@ -2181,8 +2180,8 @@ name = "opentelemetry-api"
|
||||
version = "1.39.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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'" },
|
||||
{ name = "importlib-metadata", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/0b/e5428c009d4d9af0515b0a8371a8aaae695371af291f45e702f7969dce6b/opentelemetry_api-1.39.0.tar.gz", hash = "sha256:6130644268c5ac6bdffaf660ce878f10906b3e789f7e2daa5e169b047a2933b9", size = 65763, upload-time = "2025-12-03T13:19:56.378Z" }
|
||||
wheels = [
|
||||
@@ -2194,7 +2193,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' and python_full_version < '3.14'" },
|
||||
{ name = "opentelemetry-proto", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
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 = [
|
||||
@@ -2206,13 +2205,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' 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'" },
|
||||
{ name = "googleapis-common-protos", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-common", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-proto", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-sdk", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "requests", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/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 = [
|
||||
@@ -2224,7 +2223,7 @@ name = "opentelemetry-proto"
|
||||
version = "1.39.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "protobuf", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
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 = [
|
||||
@@ -2236,9 +2235,9 @@ name = "opentelemetry-sdk"
|
||||
version = "1.39.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/51/e3/7cd989003e7cde72e0becfe830abff0df55c69d237ee7961a541e0167833/opentelemetry_sdk-1.39.0.tar.gz", hash = "sha256:c22204f12a0529e07aa4d985f1bca9d6b0e7b29fe7f03e923548ae52e0e15dde", size = 171322, upload-time = "2025-12-03T13:20:09.651Z" }
|
||||
wheels = [
|
||||
@@ -2250,8 +2249,8 @@ name = "opentelemetry-semantic-conventions"
|
||||
version = "0.60b0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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'" },
|
||||
{ name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/0e/176a7844fe4e3cb5de604212094dffaed4e18b32f1c56b5258bcbcba85c2/opentelemetry_semantic_conventions-0.60b0.tar.gz", hash = "sha256:227d7aa73cbb8a2e418029d6b6465553aa01cf7e78ec9d0bc3255c7b3ac5bf8f", size = 137935, upload-time = "2025-12-03T13:20:12.395Z" }
|
||||
wheels = [
|
||||
@@ -3432,9 +3431,9 @@ name = "sse-starlette"
|
||||
version = "2.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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'" },
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "starlette", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "uvicorn", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/fc/56ab9f116b2133521f532fce8d03194cf04dcac25f583cf3d839be4c0496/sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169", size = 19678, upload-time = "2024-08-01T08:52:50.248Z" }
|
||||
wheels = [
|
||||
@@ -3460,7 +3459,7 @@ name = "starlette"
|
||||
version = "0.50.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" }
|
||||
@@ -3704,8 +3703,8 @@ name = "uvicorn"
|
||||
version = "0.38.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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'" },
|
||||
{ name = "click", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "h11", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
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 = [
|
||||
@@ -3781,7 +3780,7 @@ name = "watchfiles"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
||||
{ name = "anyio", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
|
||||
wheels = [
|
||||
|
||||
@@ -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.5"
|
||||
version = "1.0.4"
|
||||
source = { editable = "../langgraph" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -402,7 +402,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "3.0.2"
|
||||
version = "3.0.1"
|
||||
source = { editable = "../checkpoint-postgres" }
|
||||
dependencies = [
|
||||
{ name = "langgraph-checkpoint" },
|
||||
@@ -449,7 +449,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "3.0.1"
|
||||
version = "3.0.0"
|
||||
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.3.0"
|
||||
__version__ = "0.2.14"
|
||||
|
||||
__all__ = ["Auth", "Encryption", "EncryptionContext", "get_client", "get_sync_client"]
|
||||
|
||||
@@ -1193,7 +1193,6 @@ 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:
|
||||
@@ -1202,8 +1201,6 @@ 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.
|
||||
|
||||
@@ -1215,8 +1212,6 @@ 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
|
||||
)
|
||||
@@ -4531,7 +4526,6 @@ 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:
|
||||
@@ -4540,8 +4534,6 @@ 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.
|
||||
|
||||
@@ -4553,8 +4545,6 @@ 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,8 +519,6 @@ 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,7 +103,6 @@ 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_sse():
|
||||
def test_stream_see():
|
||||
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}, id="1"),
|
||||
StreamPart(event="values", data={"step": 2}, id="2"),
|
||||
StreamPart(event="end", data=None, id="2"),
|
||||
StreamPart(event="values", data={"step": 1}),
|
||||
StreamPart(event="values", data={"step": 2}),
|
||||
StreamPart(event="end", data=None), # ty: ignore
|
||||
]
|
||||
|
||||
|
||||
@@ -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}, id="1"),
|
||||
StreamPart(event="values", data={"step": 2}, id="2"),
|
||||
StreamPart(event="end", data=None, id="2"),
|
||||
StreamPart(event="values", data={"step": 1}),
|
||||
StreamPart(event="values", data={"step": 2}),
|
||||
StreamPart(event="end", data=None),
|
||||
]
|
||||
|
||||
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# 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