mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-27 01:52:25 +02:00
Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90195af1d8 | ||
|
|
da707343dc | ||
|
|
1f5fc505c6 | ||
|
|
4c655f841e | ||
|
|
be27c96f3c | ||
|
|
5090e30f71 | ||
|
|
688de89864 | ||
|
|
62444d4c63 | ||
|
|
4a970cca8b | ||
|
|
18b9135770 | ||
|
|
d266ddb312 | ||
|
|
7a348ac19c | ||
|
|
123d93539a | ||
|
|
785e7dab3a | ||
|
|
582fb11dd4 | ||
|
|
39d9cdbef0 | ||
|
|
6dacd1aabe | ||
|
|
91ad8b803b | ||
|
|
a0e99f704f | ||
|
|
def3e06b4a | ||
|
|
a4fab2a867 | ||
|
|
cf1c7f3673 | ||
|
|
6c05b66c7c | ||
|
|
5c44dcef81 | ||
|
|
7fd6b1b4be | ||
|
|
6202e0f1d9 | ||
|
|
ccfeafa975 | ||
|
|
83238f51d8 | ||
|
|
05f008cbfb | ||
|
|
a8ae2a52a3 | ||
|
|
aa245a8e71 | ||
|
|
bdc75a22d5 | ||
|
|
69227daff3 | ||
|
|
dc8260bb72 |
@@ -82,7 +82,9 @@ jobs:
|
||||
--check-links-ignore "https://github\.com/.*" \
|
||||
--check-links-ignore "/.*\.(ipynb|html)$" \
|
||||
--check-links-ignore "https://python\.langchain\.com/.*" \
|
||||
--check-links $(find docs/site -name "index.html" | grep -v 'storm/index.html')
|
||||
--check-links-ignore "https://openai.com/index/memory-and-new-controls-for-chatgpt/" \
|
||||
--check-links $(find docs/site -name "index.html" | grep -v 'storm/index.html')
|
||||
|
||||
else
|
||||
echo "Fetching changes from origin/main..."
|
||||
git fetch origin main
|
||||
|
||||
@@ -11,7 +11,7 @@ LangGraph Cloud is available within <a href="https://www.langchain.com/langsmith
|
||||
|
||||
Starting from the <a href="https://smith.langchain.com/" target="_blank">LangSmith UI</a>...
|
||||
|
||||
1. In the left-hand navigation panel, select `Deployments`. The `Deployments` view contains a list of existing LangGraph Cloud deployments.
|
||||
1. In the left-hand navigation panel, select `LangGraph Cloud`. The `LangGraph Cloud` view contains a list of existing LangGraph Cloud deployments.
|
||||
1. In the top-right corner, select `+ New Deployment` to create a new deployment.
|
||||
1. In the `Create New Deployment` panel, fill out the required fields.
|
||||
1. `Deployment details`
|
||||
@@ -38,7 +38,7 @@ When [creating a new deployment](#create-new-deployment), a new revision is crea
|
||||
|
||||
Starting from the <a href="https://smith.langchain.com/" target="_blank">LangSmith UI</a>...
|
||||
|
||||
1. In the left-hand navigation panel, select `Deployments`. The `Deployments` view contains a list of existing LangGraph Cloud deployments.
|
||||
1. In the left-hand navigation panel, select `LangGraph Cloud`. The `LangGraph Cloud` view contains a list of existing LangGraph Cloud deployments.
|
||||
1. Select an existing deployment to create a new revision for.
|
||||
1. In the `Deployment` view, in the top-right corner, select `+ New Revision`.
|
||||
1. In the `New Revision` modal, fill out the required fields.
|
||||
@@ -56,7 +56,7 @@ Starting from the <a href="https://smith.langchain.com/" target="_blank">LangSmi
|
||||
|
||||
Build and deployment logs are available for each revision.
|
||||
|
||||
Starting from the `Deployment` view...
|
||||
Starting from the `LangGraph Cloud` view...
|
||||
|
||||
1. Select the desired revision from the `Revisions` table. A panel slides open from the right-hand side and the `Build` tab is selected by default, which displays build logs for the revision.
|
||||
1. In the panel, select the `Deploy` tab to view deployment logs for the revision.
|
||||
@@ -69,7 +69,7 @@ Interrupting a revision will stop deployment of the revision.
|
||||
!!! warning "Undefined Behavior"
|
||||
Interrupted revisions have undefined behavior. This is only useful if you need to deploy a new revision and you already have a revision "stuck" in progress. In the future, this feature may be removed.
|
||||
|
||||
Starting from the `Deployment` view...
|
||||
Starting from the `LangGraph Cloud` view...
|
||||
|
||||
1. Select the menu icon (three dots) on the right-hand side of the row for the desired revision from the `Revisions` table.
|
||||
1. Select `Interrupt` from the menu.
|
||||
@@ -79,13 +79,13 @@ Starting from the `Deployment` view...
|
||||
|
||||
Starting from the <a href="https://smith.langchain.com/" target="_blank">LangSmith UI</a>...
|
||||
|
||||
1. In the left-hand navigation panel, select `Deployments`. The `Deployments` view contains a list of existing LangGraph Cloud deployments.
|
||||
1. In the left-hand navigation panel, select `LangGraph Cloud`. The `LangGraph Cloud` view contains a list of existing LangGraph Cloud deployments.
|
||||
1. Select the menu icon (three dots) on the right-hand side of the row for the desired deployment and select `Delete`.
|
||||
1. A `Confirmation` modal will appear. Select `Delete`.
|
||||
|
||||
## Deployment Settings
|
||||
|
||||
Starting from the `Deployment` view...
|
||||
Starting from the `LangGraph Cloud` view...
|
||||
|
||||
1. In the top-right corner, select the gear icon (`Deployment Settings`).
|
||||
1. Update the `Git Branch` to the desired branch.
|
||||
|
||||
@@ -4,7 +4,7 @@ The LangGraph Studio UI connects directly to LangGraph Cloud deployments.
|
||||
|
||||
Starting from the <a href="https://smith.langchain.com/" target="_blank">LangSmith UI</a>...
|
||||
|
||||
1. In the left-hand navigation panel, select `Deployments`. The `Deployments` view contains a list of existing LangGraph Cloud deployments.
|
||||
1. In the left-hand navigation panel, select `LangGraph Cloud`. The `LangGraph Cloud` view contains a list of existing LangGraph Cloud deployments.
|
||||
1. Select an existing deployment to test with LangGraph Studio.
|
||||
1. In the top-right corner, select `Open LangGraph Studio`.
|
||||
1. [Invoke an assistant](./invoke_studio.md) or [view an existing thread](./threads_studio.md).
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
hide:
|
||||
- navigation
|
||||
title: Concepts
|
||||
description: Conceptual Guide for LangGraph
|
||||
---
|
||||
|
||||
# Conceptual Guide
|
||||
|
||||
This guide provides explanations of the key concepts behind the LangGraph framework and AI applications more broadly.
|
||||
|
||||
We recommend that you go through at least the [Quick Start](../tutorials/introduction.ipynb) before diving into the conceptual guide. This will provide practical context that will make it easier to understand the concepts discussed here.
|
||||
|
||||
The conceptual guide does not cover step-by-step instructions or specific implementation examples — those are found in the [Tutorials](../tutorials/index.md) and [How-to guides](../how-tos/index.md).
|
||||
For detailed reference material, please see the [API reference](../reference/index.md).
|
||||
|
||||
## Concepts
|
||||
|
||||
- [Why LangGraph?](high_level.md): A high-level overview of LangGraph and its goals.
|
||||
- [LangGraph Glossary](low_level.md): LangGraph workflows are designed as graphs, with nodes representing different components and edges representing the flow of information between them. This guide provides an overview of the key concepts associated with LangGraph graph primitives.
|
||||
- [Common Agentic Patterns](agentic_concepts.md): An agent are LLMs that can pick its own control flow to solve more complex problems! Agents are a key building block in many LLM applications. This guide explains the different types of agent architectures and how they can be used to control the flow of an application.
|
||||
- [Multi-Agent Systems](multi_agent.md): Complex LLM applications can often be broken down into multiple agents, each responsible for a different part of the application. This guide explains common patterns for building multi-agent systems.
|
||||
- [Human-in-the-Loop](human_in_the_loop.md): Explains different ways of integrating human feedback into a LangGraph application.
|
||||
- [Persistence](persistence.md): LangGraph has a built-in persistence layer, implemented through checkpointers. This persistence layer helps to support powerful capabilities like human-in-the-loop, memory, time travel, and fault-tolerance.
|
||||
- [Memory](memory.md): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
|
||||
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
|
||||
- [FAQ](faq.md): Frequently asked questions about LangGraph.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## What is Memory?
|
||||
|
||||
Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences. This guide is divided into two sections based on the scope of memory recall: short-term memory and long-term memory.
|
||||
[Memory](https://pmc.ncbi.nlm.nih.gov/articles/PMC10410470/) is a cognitive function that allows people to store, retrieve, and use information to understand their present and future. Consider the frustration of working with a colleague who forgets everything you tell them, requiring constant repetition! As AI agents undertake more complex tasks involving numerous user interactions, equipping them with memory becomes equally crucial for efficiency and user satisfaction. With memory, agents can learn from feedback and adapt to users' preferences. This guide covers two types of memory based on recall scope:
|
||||
|
||||
**Short-term memory**, or [thread](persistence.md#threads)-scoped memory, can be recalled at any time **from within** a single conversational thread with a user. LangGraph manages short-term memory as a part of your agent's [state](low_level.md#state). State is persisted to a database using a [checkpointer](persistence.md#checkpoints) so the thread can be resumed at any time. Short-term memory updates when the graph is invoked or a step is completed, and the State is read at the start of each step.
|
||||
|
||||
@@ -173,6 +173,8 @@ trim_messages(
|
||||
|
||||
Long-term memory in LangGraph allows systems to retain information across different conversations or sessions. Unlike short-term memory, which is thread-scoped, long-term memory is saved within custom "namespaces."
|
||||
|
||||
### Storing memories
|
||||
|
||||
LangGraph stores long-term memories as JSON documents in a [store](persistence.md#memory-store) ([reference doc](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore)). Each memory is organized under a custom `namespace` (similar to a folder) and a distinct `key` (like a filename). Namespaces often include user or org IDs or other labels that makes it easier to organize information. This structure enables hierarchical organization of memories. Cross-namespace searching is then supported through content filters. See the example below for an example.
|
||||
|
||||
```python
|
||||
@@ -190,94 +192,75 @@ item = store.get(namespace, "a-memory")
|
||||
items = store.search(namespace, filter={"my-key": "my-value"})
|
||||
```
|
||||
|
||||
When adding long-term memory to your agent, it's important to think about how to **write memories**, how to **store and manage memory updates**, and how to **recall & represent memories** for the LLM in your application. These questions are all interdependent: how you want to recall & format memories for the LLM dictates what you should store and how to manage it. Furthermore, each technique has tradeoffs. The right approach for you largely depends on your application's needs.
|
||||
LangGraph aims to give you the low-level primitives to directly control the long-term memory of your application, based on memory [Store](persistence.md#memory-store)'s.
|
||||
### Framework for thinking about long-term memory
|
||||
|
||||
Long-term memory is far from a solved problem. While it is hard to provide generic advice, we have provided a few reliable patterns below for your consideration as you implement long-term memory.
|
||||
Long-term memory is a complex challenge without a one-size-fits-all solution. However, the following questions provide a structure framework to help you navigate the different techniques:
|
||||
|
||||
**Do you want to write memories "on the hot path" or "in the background"**
|
||||
**What is the type of memory?**
|
||||
|
||||
Memory can be updated either as part of your primary application logic (e.g. "on the hot path" of the application) or as a background task (as a separate function that generates memories based on the primary application's state). We document some tradeoffs for each approach in [the writing memories section below](#writing-memories).
|
||||
Humans use memories to remember [facts](https://en.wikipedia.org/wiki/Semantic_memory), [experiences](https://en.wikipedia.org/wiki/Episodic_memory), and [rules](https://en.wikipedia.org/wiki/Procedural_memory). AI agents can use memory in the same ways. For example, AI agents can use memory to remember specific facts about a user to accomplish a task. We expand on several types of memories in the [section below](#memory-types).
|
||||
|
||||
**Do you want to manage memories as a single profile or as a collection of documents?**
|
||||
**When do you want to update memories?**
|
||||
|
||||
We provide two main approaches to managing long-term memory: a single, continuously updated document (referred to as a "profile" or "schema") or a collection of documents. Each method offers its own benefits, depending on the type of information you need to store and how you intend to access it.
|
||||
Memory can be updated as part of an agent's application logic (e.g. "on the hot path"). In this case, the agent typically decides to remember facts before responding to a user. Alternatively, memory can be updated as a background task (logic that runs in the background / asynchronously and generates memories). We explain the tradeoffs between these approaches in the [section below](#writing-memories).
|
||||
|
||||
Managing memories as a single, continuously updated "profile" or "schema" is useful when there is well-scoped, specific information you want to remember about a user, organization, or other entity (including the agent itself). You can define the schema of the profile ahead of time, and then use an LLM to update this based on interactions. Querying the "memory" is easy since it's a simple GET operation on a JSON document. We explain this in more detail in [remember a profile](#manage-individual-profiles). This technique can provide higher precision (on known information use cases) at the expense of lower recall (since you have to anticipate and model your domain, and updates to the doc tend to delete or rewrite away old information at a greater frequency).
|
||||
## Memory types
|
||||
|
||||
Managing long-term memory as a collection of documents, on the other hand, lets you store an unbounded amount of information. This technique is useful when you want to repeatedly extract & remember items over a long time horizon but can be more complicated to query and manage over time.
|
||||
Similar to the "profile" memory, you still define schema(s) for each memory. Rather than overwriting a single document, you instead will insert new ones (and potentially update or re-contextualize existing ones in the process). We explain this approach in more detail in ["managing a collection of memories"](#manage-a-collection-of-memories).
|
||||
Different applications require various types of memory. Although the analogy isn't perfect, examining [human memory types](https://www.psychologytoday.com/us/basics/memory/types-of-memory?ref=blog.langchain.dev) can be insightful. Some research (e.g., the [CoALA paper](https://arxiv.org/pdf/2309.02427)) have even mapped these human memory types to those used in AI agents.
|
||||
|
||||
**Do you want to present memories to your agent as updated instructions or as few-shot examples?**
|
||||
| Memory Type | What is Stored | Human Example | Agent Example |
|
||||
|-------------|----------------|---------------|---------------|
|
||||
| Semantic | Facts | Things I learned in school | Facts about a user |
|
||||
| Episodic | Experiences | Things I did | Past agent actions |
|
||||
| Procedural | Instructions | Instincts or motor skills | Agent system prompt |
|
||||
|
||||
Memories are typically provided to the LLM as a part of the system prompt. Some common ways to "frame" memories for the LLM include providing raw information as "memories from previous interactions with user A", as system instructions or rules, or as few-shot examples.
|
||||
### Semantic Memory
|
||||
|
||||
Framing memories as "learning rules or instructions" typically means dedicating a portion of the system prompt to instructions the LLM can manage itself. After each conversation, you can prompt the LLM to evaluate its performance and update the instructions to better handle this type of task in the future. We explain this approach in more detail in [this section](#update-own-instructions).
|
||||
[Semantic memory](https://en.wikipedia.org/wiki/Semantic_memory), both in humans and AI agents, involves the retention of specific facts and concepts. In humans, it can include information learned in school and the understanding of concepts and their relationships. For AI agents, semantic memory is often used to personalize applications by remembering facts or concepts from past interactions.
|
||||
|
||||
Storing memories as few-shot examples lets you store and manage instructions as cause and effect. Each memory stores an input or context and expected response. Including a reasoning trajectory (a chain-of-thought) can also help provide sufficient context so that the memory is less likely to be mis-used in the future. We elaborate on this concept more in [this section](#few-shot-examples).
|
||||
#### Profile
|
||||
|
||||
We will expand on techniques for writing, managing, and recalling & formatting memories in the following section.
|
||||
Semantic memories can be managed in different ways. For example, memories can be a single, continuously updated "profile" of well-scoped and specific information about a user, organization, or other entity (including the agent itself). A profile is generally just a JSON document with various key-value pairs you've selected to represent your domain.
|
||||
|
||||
### Writing memories
|
||||
|
||||
Humans form long-term memories when we sleep, but when and how should our agents create new memories? The two most common ways we see agents write memories are "on the hot path" and "in the background".
|
||||
|
||||

|
||||
|
||||
#### Writing memories in the hot path
|
||||
|
||||
This involves creating memories while the application is running. To provide a popular production example, ChatGPT manages memories using a "save_memories" tool to upsert memories as content strings. It decides whether (and how) to use this tool every time it receives a user message and multi-tasks memory management with the rest of the user instructions.
|
||||
|
||||
This has a few benefits. First of all, it happens "in real time". If the user starts a new thread right away that memory will be present. The user also transparently sees when memories are stored, since the bot has to explicitly decide to store information and can relate that to the user.
|
||||
|
||||
This also has several downsides. It complicates the decisions the agent must make (what to commit to memory). This complication can degrade its tool-calling performance and reduce task completion rates. It will slow down the final response since it needs to decide what to commit to memory. It also typically leads to fewer things being saved to memory (since the assistant is multi-tasking), which will cause **lower recall** in later conversations.
|
||||
|
||||
#### Writing memories in the background
|
||||
|
||||
This involves updating memory as a conceptually separate task, typically as a completely separate graph or function. Since it happens in the background, it incurs no latency. It also splits up the application logic from the memory logic, making it more modular and easy to manage. It also lets you separate the timing of memory creation, letting you avoid redundant work. Your agent can focus on accomplishing its immediate task without having to consciously think about what it needs to remember.
|
||||
|
||||
This approach is not without its downsides, however. You have to think about how often to write memories. If it doesn't run in realtime, the user's interactions on other threads won't benefit from the new context. You also have to think about when to trigger this job. We typically recommend scheduling memories after some point of time, cancelling and re-scheduling for the future if new events occur on a given thread. Other popular choices are to form memories on some cron schedule or to let the user or application logic manually trigger memory formation.
|
||||
|
||||
### Managing memories
|
||||
|
||||
Once you've sorted out memory scheduling, it's important to think about **how to update memory with new information**.
|
||||
|
||||
There are two main approaches: you can either continuously update a single document (memory profile) or insert new documents each time you receive new information.
|
||||
|
||||
We will outline some tradeoffs between these two approaches below, understanding that most people will find it most appropriate to combine approaches and to settle somewhere in the middle.
|
||||
|
||||
#### Manage individual profiles
|
||||
|
||||
A profile is generally just a JSON document with various key-value pairs you've selected to represent your domain. When remembering a profile, you will want to make sure that you are **updating** the profile each time. As a result, you will want to pass in the previous profile and ask the LLM to generate a new profile (or some JSON patch to apply to the old profile).
|
||||
|
||||
The larger the document, the more error-prone this can become. If your document becomes **too** large, you may want to consider splitting up the profiles into separate sections. You will likely need to use generation with retries and/or **strict** decoding when generating documents to ensure the memory schemas remains valid.
|
||||
When remembering a profile, you will want to make sure that you are **updating** the profile each time. As a result, you will want to pass in the previous profile and [ask the model to generate a new profile](https://github.com/langchain-ai/memory-template) (or some [JSON patch](https://github.com/hinthornw/trustcall) to apply to the old profile). This can be become error-prone as the profile gets larger, and may benefit from splitting a profile into multiple documents or **strict** decoding when generating documents to ensure the memory schemas remains valid.
|
||||
|
||||

|
||||
|
||||
#### Manage a collection of memories
|
||||
#### Collection
|
||||
|
||||
Saving memories as a collection of documents simplifies some things. Each individual memory can be more narrowly scoped and easier to generate. It also means you're less likely to **lose** information over time, since it's easier for an LLM to generate _new_ objects for new information than it is for it to reconcile that new information with information in a dense profile. This tends to lead to higher recall downstream.
|
||||
Alternatively, memories can be a collection of documents that are continuously updated and extended over time. Each individual memory can be more narrowly scoped and easier to generate, which means that you're less likely to **lose** information over time. It's easier for an LLM to generate _new_ objects for new information than reconcile new information with an existing profile. As a result, a document collection tends to lead to [higher recall downstream](https://en.wikipedia.org/wiki/Precision_and_recall).
|
||||
|
||||
This approach shifts some complexity to how you prompt the LLM to apply memory updates. You now have to enable the LLM to _delete_ or _update_ existing items in the list. This can be tricky to prompt the LLM to do. Some LLMs may default to over-inserting; others may default to over-updating. Tuning the behavior here is best done through evals, something you can do with a tool like [LangSmith](https://docs.smith.langchain.com/tutorials/Developers/evaluation).
|
||||
However, this shifts some complexity memory updating. The model must now _delete_ or _update_ existing items in the list, which can be tricky. In addition, some models may default to over-inserting and others may default to over-updating. See the [Trustcall](https://github.com/hinthornw/trustcall) package for one way to manage this and consider evaluation (e.g., with a tool like [LangSmith](https://docs.smith.langchain.com/tutorials/Developers/evaluation)) to help you tune the behavior.
|
||||
|
||||
This also shifts complexity to memory **search** (recall). You have to think about what relevant items to use. Right now we support filtering by metadata. We will be adding semantic search shortly.
|
||||
Working with document collections also shifts complexity to memory **search** over the list. The `Store` currently supports [filtering by metadata](https://langchain-ai.github.io/langgraph/reference/store/#storage) and will soon add [semantic search shortly](https://python.langchain.com/docs/concepts/vectorstores/), but selecting the most relevant documents can be tricky as the list grows.
|
||||
|
||||
Finally, this shifts some complexity to how you represent the memories for the LLM (and by extension, the schemas you use to save each memories). It's very easy to write memories that can easily be mistaken out-of-context. It's important to prompt the LLM to include all necessary contextual information in the given memory so that when you use it in later conversations it doesn't mistakenly mis-apply that information.
|
||||
Finally, using a collection of memories can make it challenging to provide comprehensive context to the model. While individual memories may follow a specific schema, this structure might not capture the full context or relationships between memories. As a result, when using these memories to generate responses, the model may lack important contextual information that would be more readily available in a unified profile approach.
|
||||
|
||||

|
||||
|
||||
### Representing memories
|
||||
Regardless of memory management approach, the central point is that the agent will use the semantic memories to [ground its responses](https://python.langchain.com/docs/concepts/rag/), which often leads to more personalized and relevant interactions.
|
||||
|
||||
Once you have saved memories, the way you then retrieve and present the memory content for the LLM can play a large role in how well your LLM incorporates that information in its responses.
|
||||
The following sections present a couple of common approaches. Note that these sections also will largely inform how you write and manage memories. Everything in memory is connected!
|
||||
### Episodic Memory
|
||||
|
||||
#### Update own instructions
|
||||
[Episodic memory](https://en.wikipedia.org/wiki/Episodic_memory), in both humans and AI agents, involves recalling past events or actions. The [CoALA paper](https://arxiv.org/pdf/2309.02427) frames this well: facts can be written to semantic memory, whereas *experiences* can be written to episodic memory. For AI agents, episodic memory is often used to help an agent remember how to accomplish a task.
|
||||
|
||||
While instructions are often static text written by the developer, many AI applications benefit from letting the users personalize the rules and instructions the agent should follow whenever it interacts with that user. This ideally can be inferred by its interactions with the user (so the user doesn't have to explicitly change settings in yoru app). In this sense, instructions are a form of long-form memory!
|
||||
In practice, episodic memories are often implemented through [few-shot example prompting](https://python.langchain.com/docs/concepts/few_shot_prompting/), where agents learn from past sequences to perform tasks correctly. Sometimes it's easier to "show" than "tell" and LLMs learn well from examples. Few-shot learning lets you ["program"](https://x.com/karpathy/status/1627366413840322562) your LLM by updating the prompt with input-output examples to illustrate the intended behavior. While various [best-practices](https://python.langchain.com/docs/concepts/#1-generating-examples) can be used to generate few-shot examples, often the challenge lies in selecting the most relevant examples based on user input.
|
||||
|
||||
One way to apply this is using "reflection" or "Meta-prompting" steps. Prompt the LLM with the current instruction set (from the system prompt) and a conversation with the user, and instruct the LLM to refine its instructions. This approach allows the system to dynamically update and improve its own behavior, potentially leading to better performance on various tasks. This is particularly useful for tasks where the instructions are challenging to specify a priori.
|
||||
Note that the memory [store](persistence.md#memory-store) is just one way to store data as few-shot examples. If you want to have more developer involvement, or tie few-shots more closely to your evaluation harness, you can also use a [LangSmith Dataset](https://docs.smith.langchain.com/evaluation/how_to_guides/datasets/index_datasets_for_dynamic_few_shot_example_selection) to store your data. Then dynamic few-shot example selectors can be used out-of-the box to achieve this same goal. LangSmith will index the dataset for you and enable retrieval of few shot examples that are most relevant to the user input based upon keyword similarity ([using a BM25-like algorithm](https://docs.smith.langchain.com/how_to_guides/datasets/index_datasets_for_dynamic_few_shot_example_selection) for keyword based similarity).
|
||||
|
||||
Meta-prompting uses past information to refine prompts. For instance, a [Tweet generator](https://www.youtube.com/watch?v=Vn8A3BxfplE) employs meta-prompting to enhance its paper summarization prompt for Twitter. You could implement this using LangGraph's memory store to save updated instructions in a shared namespace. In this case, we will namespace the memories as "agent_instructions" and key the memory based on the agent.
|
||||
See this how-to [video](https://www.youtube.com/watch?v=37VaU7e7t5o) for example usage of dynamic few-shot example selection in LangSmith. Also, see this [blog post](https://blog.langchain.dev/few-shot-prompting-to-improve-tool-calling-performance/) showcasing few-shot prompting to improve tool calling performance and this [blog post](https://blog.langchain.dev/aligning-llm-as-a-judge-with-human-preferences/) using few-shot example to align an LLMs to human preferences.
|
||||
|
||||
### Procedural Memory
|
||||
|
||||
[Procedural memory](https://en.wikipedia.org/wiki/Procedural_memory), in both humans and AI agents, involves remembering the rules used to perform tasks. In humans, procedural memory is like the internalized knowledge of how to perform tasks, such as riding a bike via basic motor skills and balance. Episodic memory, on the other hand, involves recalling specific experiences, such as the first time you successfully rode a bike without training wheels or a memorable bike ride through a scenic route. For AI agents, procedural memory is a combination of model weights, agent code, and agent's prompt that collectively determine the agent's functionality.
|
||||
|
||||
In practice, it is fairly uncommon for agents to modify their model weights or rewrite their code. However, it is more common for agents to [modify their own prompts](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/prompt-generator).
|
||||
|
||||
One effective approach to refining an agent's instructions is through ["Reflection"](https://blog.langchain.dev/reflection-agents/) or meta-prompting. This involves prompting the agent with its current instructions (e.g., the system prompt) along with recent conversations or explicit user feedback. The agent then refines its own instructions based on this input. This method is particularly useful for tasks where instructions are challenging to specify upfront, as it allows the agent to learn and adapt from its interactions.
|
||||
|
||||
For example, we built a [Tweet generator](https://www.youtube.com/watch?v=Vn8A3BxfplE) using external feedback and prompt re-writing to produce high-quality paper summaries for Twitter. In this case, the specific summarization prompt was difficult to specify *a priori*, but it was fairly easy for a user to critique the generated Tweets and provide feedback on how to improve the summarization process.
|
||||
|
||||
The below pseudo-code shows how you might implement this with the LangGraph memory [store](persistence.md#memory-store), using the store to save a prompt, the `update_instructions` node to get the current prompt (as well as feedback from the conversation with the user captured in `state["messages"]`), update the prompt, and save the new prompt back to the store. Then, the `call_model` get the updated prompt from the store and uses it to generate a response.
|
||||
|
||||
```python
|
||||
# Node that *uses* the instructions
|
||||
@@ -288,7 +271,6 @@ def call_model(state: State, store: BaseStore):
|
||||
prompt = prompt_template.format(instructions=instructions.value["instructions"])
|
||||
...
|
||||
|
||||
|
||||
# Node that updates instructions
|
||||
def update_instructions(state: State, store: BaseStore):
|
||||
namespace = ("instructions",)
|
||||
@@ -303,8 +285,24 @@ def update_instructions(state: State, store: BaseStore):
|
||||
|
||||

|
||||
|
||||
#### Few-shot examples
|
||||
## Writing memories
|
||||
|
||||
Sometimes it's easier to "show" than "tell." LLMs learn well from examples. Few-shot learning lets you ["program"](https://x.com/karpathy/status/1627366413840322562) your LLM by updating the prompt with input-output examples to illustrate the intended behavior. While various [best-practices](https://python.langchain.com/docs/concepts/#1-generating-examples) can be used to generate few-shot examples, often the challenge lies in selecting the most relevant examples based on user input.
|
||||
While [humans often form long-term memories during sleep](https://medicine.yale.edu/news-article/sleeps-crucial-role-in-preserving-memory/), AI agents need a different approach. When and how should agents create new memories? There are at least two primary methods for agents to write memories: "on the hot path" and "in the background".
|
||||
|
||||
Note that the memory store is just one way to store data as few-shot examples. If you want to have more developer involvement, or tie few-shots more closely to your evaluation harness, you can also use a [LangSmith Dataset](https://docs.smith.langchain.com/how_to_guides/datasets) to store your data. Then dynamic few-shot example selectors can be used out-of-the box to achieve this same goal. LangSmith will index the dataset for you and enable retrieval of few shot examples that are most relevant to the user input based upon keyword similarity ([using a BM25-like algorithm](https://docs.smith.langchain.com/how_to_guides/datasets/index_datasets_for_dynamic_few_shot_example_selection) for keyword based similarity). See this how-to [video](https://www.youtube.com/watch?v=37VaU7e7t5o) for example usage of dynamic few-shot example selection in LangSmith. Also, see this [blog post](https://blog.langchain.dev/few-shot-prompting-to-improve-tool-calling-performance/) showcasing few-shot prompting to improve tool calling performance and this [blog post](https://blog.langchain.dev/aligning-llm-as-a-judge-with-human-preferences/) using few-shot example to align an LLMs to human preferences.
|
||||

|
||||
|
||||
### Writing memories in the hot path
|
||||
|
||||
Creating memories during runtime offers both advantages and challenges. On the positive side, this approach allows for real-time updates, making new memories immediately available for use in subsequent interactions. It also enables transparency, as users can be notified when memories are created and stored.
|
||||
|
||||
However, this method also presents challenges. It may increase complexity if the agent requires a new tool to decide what to commit to memory. In addition, the process of reasoning about what to save to memory can impact agent latency. Finally, the agent must multitask between memory creation and its other responsibilities, potentially affecting the quantity and quality of memories created.
|
||||
|
||||
As an example, ChatGPT uses a [save_memories](https://openai.com/index/memory-and-new-controls-for-chatgpt/) tool to upsert memories as content strings, deciding whether and how to use this tool with each user message. See our [memory-agent](https://github.com/langchain-ai/memory-agent) template as an reference implementation.
|
||||
|
||||
### Writing memories in the background
|
||||
|
||||
Creating memories as a separate background task offers several advantages. It eliminates latency in the primary application, separates application logic from memory management, and allows for more focused task completion by the agent. This approach also provides flexibility in timing memory creation to avoid redundant work.
|
||||
|
||||
However, this method has its own challenges. Determining the frequency of memory writing becomes crucial, as infrequent updates may leave other threads without new context. Deciding when to trigger memory formation is also important. Common strategies include scheduling after a set time period (with rescheduling if new events occur), using a cron schedule, or allowing manual triggers by users or the application logic.
|
||||
|
||||
See our [memory-service](https://github.com/langchain-ai/memory-template) template as an reference implementation.
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"execution_count": 2,
|
||||
"id": "09372b8b-edea-4b9d-9ec3-3d93ce1ba819",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -132,7 +132,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": 3,
|
||||
"id": "66f52a20",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -163,7 +163,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": 4,
|
||||
"id": "38846b01",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -183,7 +183,7 @@
|
||||
"{'aggregate': [\"I'm A\", \"I'm B\", \"I'm C\", \"I'm D\"]}"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
@@ -220,7 +220,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"execution_count": 5,
|
||||
"id": "259a7704-5aa0-4e4c-aeef-cca04e8be0ff",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -238,6 +238,15 @@
|
||||
" aggregate: Annotated[list, operator.add]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class ReturnNodeValue:\n",
|
||||
" def __init__(self, node_secret: str):\n",
|
||||
" self._value = node_secret\n",
|
||||
"\n",
|
||||
" def __call__(self, state: State) -> Any:\n",
|
||||
" print(f\"Adding {self._value} to {state['aggregate']}\")\n",
|
||||
" return {\"aggregate\": [self._value]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"builder = StateGraph(State)\n",
|
||||
"builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n",
|
||||
"builder.add_edge(START, \"a\")\n",
|
||||
@@ -255,7 +264,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"execution_count": 6,
|
||||
"id": "83320227-8ab3-44c0-b6cf-064a7a425b9f",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -278,7 +287,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"execution_count": 7,
|
||||
"id": "3f971fa3-29e4-466f-a85e-2863bfecf7fe",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -299,7 +308,7 @@
|
||||
"{'aggregate': [\"I'm A\", \"I'm B\", \"I'm C\", \"I'm B2\", \"I'm D\"]}"
|
||||
]
|
||||
},
|
||||
"execution_count": 8,
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
@@ -322,7 +331,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"execution_count": 8,
|
||||
"id": "95f5e026",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -341,6 +350,15 @@
|
||||
" which: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class ReturnNodeValue:\n",
|
||||
" def __init__(self, node_secret: str):\n",
|
||||
" self._value = node_secret\n",
|
||||
"\n",
|
||||
" def __call__(self, state: State) -> Any:\n",
|
||||
" print(f\"Adding {self._value} to {state['aggregate']}\")\n",
|
||||
" return {\"aggregate\": [self._value]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"builder = StateGraph(State)\n",
|
||||
"builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n",
|
||||
"builder.add_edge(START, \"a\")\n",
|
||||
@@ -372,7 +390,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"execution_count": 9,
|
||||
"id": "1d0e6c56",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -395,7 +413,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"execution_count": 10,
|
||||
"id": "7134f652",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -415,7 +433,7 @@
|
||||
"{'aggregate': [\"I'm A\", \"I'm B\", \"I'm C\", \"I'm E\"], 'which': 'bc'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 11,
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
@@ -426,7 +444,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"execution_count": 11,
|
||||
"id": "b130e694",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -446,7 +464,7 @@
|
||||
"{'aggregate': [\"I'm A\", \"I'm C\", \"I'm D\", \"I'm E\"], 'which': 'cd'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 12,
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
@@ -471,7 +489,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"execution_count": 12,
|
||||
"id": "836bc12d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -564,7 +582,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"execution_count": 13,
|
||||
"id": "932c497e",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -587,7 +605,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 15,
|
||||
"execution_count": 14,
|
||||
"id": "933b3afd",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -608,7 +626,7 @@
|
||||
" 'which': 'bc'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 15,
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
@@ -619,7 +637,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 16,
|
||||
"execution_count": 15,
|
||||
"id": "e30531bf",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -640,7 +658,7 @@
|
||||
" 'which': 'cd'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 16,
|
||||
"execution_count": 15,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
@@ -666,7 +684,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.4"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
+28
-12
@@ -1,15 +1,18 @@
|
||||
---
|
||||
hide:
|
||||
- toc
|
||||
- navigation
|
||||
title: How-to Guides
|
||||
description: How to accomplish common tasks in LangGraph
|
||||
---
|
||||
|
||||
# How-to guides
|
||||
# How-to Guides
|
||||
|
||||
Welcome to the LangGraph how-to guides! These guides provide practical, step-by-step instructions for accomplishing key tasks in LangGraph.
|
||||
|
||||
## Controllability
|
||||
|
||||
LangGraph is known for being a highly controllable agent framework.
|
||||
LangGraph offers a high level of control over the execution of your graph.
|
||||
|
||||
These how-to guides show how to achieve that controllability.
|
||||
|
||||
- [How to create branches for parallel execution](branching.ipynb)
|
||||
@@ -18,7 +21,7 @@ These how-to guides show how to achieve that controllability.
|
||||
|
||||
## Persistence
|
||||
|
||||
LangGraph makes it easy to persist state across graph runs (thread-level persistence) and across threads (cross-thread persistence). These how-to guides show how to add persistence to your graph.
|
||||
[LangGraph Persistence](../concepts/persistence.md) makes it easy to persist state across graph runs (thread-level persistence) and across threads (cross-thread persistence). These how-to guides show how to add persistence to your graph.
|
||||
|
||||
- [How to add thread-level persistence to your graph](persistence.ipynb)
|
||||
- [How to add thread-level persistence to subgraphs](subgraph-persistence.ipynb)
|
||||
@@ -37,8 +40,8 @@ LangGraph makes it easy to manage conversation [memory](../concepts/memory.md) i
|
||||
|
||||
## Human in the Loop
|
||||
|
||||
One of LangGraph's main benefits is that it makes human-in-the-loop workflows easy.
|
||||
These guides cover common examples of that.
|
||||
[Human-in-the-loop](../concepts/human_in_the_loop.md) functionality allows
|
||||
you to involve humans in the decision-making process of your graph. These how-to guides show how to implement human-in-the-loop workflows in your graph.
|
||||
|
||||
- [How to add breakpoints](human_in_the_loop/breakpoints.ipynb)
|
||||
- [How to add dynamic breakpoints](human_in_the_loop/dynamic_breakpoints.ipynb)
|
||||
@@ -49,8 +52,7 @@ These guides cover common examples of that.
|
||||
|
||||
## Streaming
|
||||
|
||||
LangGraph is built to be streaming first.
|
||||
These guides show how to use different streaming modes.
|
||||
[Streaming](../concepts/streaming.md) is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
|
||||
|
||||
- [How to stream full state of your graph](stream-values.ipynb)
|
||||
- [How to stream state updates of your graph](stream-updates.ipynb)
|
||||
@@ -66,6 +68,10 @@ These guides show how to use different streaming modes.
|
||||
|
||||
## Tool calling
|
||||
|
||||
[Tool calling](https://python.langchain.com/docs/concepts/tool_calling/) is a type of chat model API that accepts tool schemas, along with messages, as input and returns invocations of those tools as part of the output message.
|
||||
|
||||
These how-to guides show common patterns for tool calling with LangGraph:
|
||||
|
||||
- [How to call tools using ToolNode](tool-calling.ipynb)
|
||||
- [How to handle tool calling errors](tool-calling-errors.ipynb)
|
||||
- [How to pass runtime values to tools](pass-run-time-values-to-tools.ipynb)
|
||||
@@ -74,6 +80,8 @@ These guides show how to use different streaming modes.
|
||||
|
||||
## Subgraphs
|
||||
|
||||
[Subgraphs](../concepts/low_level.md#subgraphs) allow you to reuse an existing graph from another graph. These how-to guides show how to use subgraphs:
|
||||
|
||||
- [How to add and use subgraphs](subgraph.ipynb)
|
||||
- [How to view and update state in subgraphs](subgraphs-manage-state.ipynb)
|
||||
- [How to transform inputs and outputs of a subgraph](subgraph-transform-state.ipynb)
|
||||
@@ -97,8 +105,11 @@ These guides show how to use different streaming modes.
|
||||
|
||||
## Prebuilt ReAct Agent
|
||||
|
||||
These guides show how to use the prebuilt ReAct agent.
|
||||
Please note that here will we use a **prebuilt agent**. One of the big benefits of LangGraph is that you can easily create your own agent architectures. So while it's fine to start here to build an agent quickly, we would strongly recommend learning how to build your own agent so that you can take full advantage of LangGraph.
|
||||
The LangGraph [prebuilt ReAct agent](../reference/prebuilt.md#langgraph.prebuilt.chat_agent_executor.create_react_agent) is pre-built implementation of a [tool calling agent](../concepts/agentic_concepts.md#tool-calling-agent).
|
||||
|
||||
One of the big benefits of LangGraph is that you can easily create your own agent architectures. So while it's fine to start here to build an agent quickly, we would strongly recommend learning how to build your own agent so that you can take full advantage of LangGraph.
|
||||
|
||||
These guides show how to use the prebuilt ReAct agent:
|
||||
|
||||
- [How to create a ReAct agent](create-react-agent.ipynb)
|
||||
- [How to add memory to a ReAct agent](create-react-agent-memory.ipynb)
|
||||
@@ -108,6 +119,11 @@ Please note that here will we use a **prebuilt agent**. One of the big benefits
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Errors
|
||||
The [Error Reference](../troubleshooting/errors/index.md) page contains guides around resolving common errors you may find while building with LangChain. Errors referenced below will have an `lc_error_code` property corresponding to one of the below codes when they are thrown in code.
|
||||
|
||||
- [GRAPH_RECURSION_LIMIT](../troubleshooting/errors/GRAPH_RECURSION_LIMIT.md)
|
||||
- [INVALID_CONCURRENT_GRAPH_UPDATE](../troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md)
|
||||
- [INVALID_GRAPH_NODE_RETURN_VALUE](../troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md)
|
||||
- [MULTIPLE_SUBGRAPHS](../troubleshooting/errors/MULTIPLE_SUBGRAPHS.md)
|
||||
|
||||
|
||||
- [Error reference](../troubleshooting/errors/index.md)
|
||||
|
||||
@@ -3,7 +3,6 @@ hide_comments: true
|
||||
hide:
|
||||
- navigation
|
||||
title: Home
|
||||
|
||||
---
|
||||
|
||||
{!README.md!}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Reference
|
||||
description: API reference for LangGraph
|
||||
---
|
||||
|
||||
# Reference
|
||||
|
||||
Welcome to the LangGraph API reference! This reference provides detailed information about the LangGraph API, including classes, methods, and other components.
|
||||
|
||||
If you are new to LangGraph, we recommend starting with the [Quick Start](../tutorials/introduction.ipynb) in the Tutorials section.
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
hide:
|
||||
- toc
|
||||
- navigation
|
||||
title: Tutorials
|
||||
---
|
||||
|
||||
# Tutorials
|
||||
@@ -17,29 +18,33 @@ Learn the basics of LangGraph through a comprehensive quick start in which you w
|
||||
|
||||
Learn from example implementations of graphs designed for specific scenarios and that implement common design patterns.
|
||||
|
||||
#### Chatbots
|
||||
### Chatbots
|
||||
|
||||
- [Customer Support](customer-support/customer-support.ipynb): Build a customer support chatbot to manage flights, hotel reservations, car rentals, and other tasks
|
||||
- [Prompt Generation from User Requirements](chatbots/information-gather-prompting.ipynb): Build an information gathering chatbot
|
||||
- [Code Assistant](code_assistant/langgraph_code_assistant.ipynb): Build a code analysis and generation assistant
|
||||
|
||||
|
||||
### RAG
|
||||
|
||||
- [Agentic RAG](rag/langgraph_agentic_rag.ipynb): Use an agent to figure out how to retrieve the most relevant information before using the retrieved information to answer the user's question.
|
||||
- [Adaptive RAG](rag/langgraph_adaptive_rag.ipynb): Adaptive RAG is a strategy for RAG that unites (1) query analysis with (2) active / self-corrective RAG. Implementation of: https://arxiv.org/abs/2403.14403
|
||||
- For a version that uses a local LLM: [Adaptive RAG using local LLMs](rag/langgraph_adaptive_rag_local.ipynb)
|
||||
- [Corrective RAG](rag/langgraph_crag.ipynb): Uses an LLM to grade the quality of the retrieved information from the given source, and if the quality is low, it will try to retrieve the information from another source. Implementation of: https://arxiv.org/pdf/2401.15884.pdf
|
||||
- For a version that uses a local LLM: [Corrective RAG using local LLMs](rag/langgraph_crag_local.ipynb)
|
||||
- [Self-RAG](rag/langgraph_self_rag.ipynb): Self-RAG is a strategy for RAG that incorporates self-reflection / self-grading on retrieved documents and generations. Implementation of https://arxiv.org/abs/2310.11511.
|
||||
- For a version that uses a local LLM: [Self-RAG using local LLMs](rag/langgraph_self_rag_local.ipynb)
|
||||
- [SQL Agent](sql-agent.ipynb): Build a SQL agent that can answer questions about a SQL database.
|
||||
|
||||
|
||||
### Agent Architectures
|
||||
|
||||
#### Multi-Agent Systems
|
||||
|
||||
- [Network](multi_agent/multi-agent-collaboration.ipynb): Enable two or more agents to collaborate on a task
|
||||
- [Supervisor](multi_agent/agent_supervisor.ipynb): Use an LLM to orchestrate and delegate to individual agents
|
||||
- [Hierarchical Teams](multi_agent/hierarchical_agent_teams.ipynb): Orchestrate nested teams of agents to solve problems
|
||||
|
||||
#### RAG
|
||||
|
||||
- [Adaptive RAG](rag/langgraph_adaptive_rag.ipynb)
|
||||
- [Adaptive RAG using local LLMs](rag/langgraph_adaptive_rag_local.ipynb)
|
||||
- [Agentic RAG](rag/langgraph_agentic_rag.ipynb)
|
||||
- [Corrective RAG](rag/langgraph_crag.ipynb)
|
||||
- [Corrective RAG using local LLMs](rag/langgraph_crag_local.ipynb)
|
||||
- [Self-RAG](rag/langgraph_self_rag.ipynb)
|
||||
- [Self-RAG using local LLMs](rag/langgraph_self_rag_local.ipynb)
|
||||
- [SQL Agent](sql-agent.ipynb)
|
||||
|
||||
|
||||
#### Planning Agents
|
||||
|
||||
- [Plan-and-Execute](plan-and-execute/plan-and-execute.ipynb): Implement a basic planning and execution agent
|
||||
@@ -53,12 +58,12 @@ Learn from example implementations of graphs designed for specific scenarios and
|
||||
- [Language Agent Tree Search](lats/lats.ipynb): Use reflection and rewards to drive a tree search over agents
|
||||
- [Self-Discover Agent](self-discover/self-discover.ipynb): Analyze an agent that learns about its own capabilities
|
||||
|
||||
#### Evaluation
|
||||
### Evaluation
|
||||
|
||||
- [Agent-based](chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb): Evaluate chatbots via simulated user interactions
|
||||
- [In LangSmith](chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb): Evaluate chatbots in LangSmith over a dialog dataset
|
||||
|
||||
#### Experimental
|
||||
### Experimental
|
||||
|
||||
- [Web Research (STORM)](storm/storm.ipynb): Generate Wikipedia-like articles via research and multi-perspective QA
|
||||
- [TNT-LLM](tnt-llm/tnt-llm.ipynb): Build rich, interpretable taxonomies of user intentand using the classification system developed by Microsoft for their Bing Copilot application.
|
||||
|
||||
+13
-122
@@ -26,6 +26,7 @@ theme:
|
||||
- navigation.instant.progress
|
||||
- navigation.prune
|
||||
- navigation.tabs
|
||||
- navigation.tabs.sticky
|
||||
- navigation.top
|
||||
- navigation.tracking
|
||||
- search.highlight
|
||||
@@ -79,128 +80,11 @@ plugins:
|
||||
- "!^_"
|
||||
nav:
|
||||
- "index.md"
|
||||
- Tutorials:
|
||||
- "tutorials/index.md"
|
||||
- Quick Start: tutorials/introduction.ipynb
|
||||
- Chatbots:
|
||||
- Customer Support: tutorials/customer-support/customer-support.ipynb
|
||||
- Prompt Generation from User Requirements: tutorials/chatbots/information-gather-prompting.ipynb
|
||||
- Code Assistant: tutorials/code_assistant/langgraph_code_assistant.ipynb
|
||||
- RAG:
|
||||
- Adaptive RAG: tutorials/rag/langgraph_adaptive_rag.ipynb
|
||||
- Adaptive RAG using local LLMs: tutorials/rag/langgraph_adaptive_rag_local.ipynb
|
||||
- Agentic RAG: tutorials/rag/langgraph_agentic_rag.ipynb
|
||||
- Corrective RAG (CRAG): tutorials/rag/langgraph_crag.ipynb
|
||||
- Corrective RAG (CRAG) using local LLMs: tutorials/rag/langgraph_crag_local.ipynb
|
||||
- Self-RAG: tutorials/rag/langgraph_self_rag.ipynb
|
||||
- Self-RAG using local LLMs: tutorials/rag/langgraph_self_rag_local.ipynb
|
||||
- SQL Agent: tutorials/sql-agent.ipynb
|
||||
- Agent Architectures:
|
||||
- Multi-Agent Systems:
|
||||
- Network: tutorials/multi_agent/multi-agent-collaboration.ipynb
|
||||
- Supervisor: tutorials/multi_agent/agent_supervisor.ipynb
|
||||
- Hierarchical Teams: tutorials/multi_agent/hierarchical_agent_teams.ipynb
|
||||
- Planning Agents:
|
||||
- Plan-and-Execute: tutorials/plan-and-execute/plan-and-execute.ipynb
|
||||
- Reasoning without Observation: tutorials/rewoo/rewoo.ipynb
|
||||
- LLMCompiler: tutorials/llm-compiler/LLMCompiler.ipynb
|
||||
- Reflection & Critique:
|
||||
- Basic Reflection: tutorials/reflection/reflection.ipynb
|
||||
- Reflexion: tutorials/reflexion/reflexion.ipynb
|
||||
- Language Agent Tree Search: tutorials/lats/lats.ipynb
|
||||
- Self-Discover Agent: tutorials/self-discover/self-discover.ipynb
|
||||
- Evaluation & Analysis:
|
||||
- Chatbot Evaluation via Simulation:
|
||||
- Agent-based: tutorials/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb
|
||||
- In LangSmith: tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb
|
||||
- Experimental:
|
||||
- Web Research (STORM): tutorials/storm/storm.ipynb
|
||||
- TNT-LLM: tutorials/tnt-llm/tnt-llm.ipynb
|
||||
- Web Navigation: tutorials/web-navigation/web_voyager.ipynb
|
||||
- Competitive Programming: tutorials/usaco/usaco.ipynb
|
||||
- Extract structured output: tutorials/extraction/retries.ipynb
|
||||
|
||||
- "How-to Guides":
|
||||
- "how-tos/index.md"
|
||||
- Controllability:
|
||||
- Create branches for parallel execution: how-tos/branching.ipynb
|
||||
- Create map-reduce branches for parallel execution: how-tos/map-reduce.ipynb
|
||||
- Control graph recursion limit: how-tos/recursion-limit.ipynb
|
||||
- Persistence:
|
||||
- Add thread-level persistence: how-tos/persistence.ipynb
|
||||
- Add thread-level persistence to subgraphs: how-tos/subgraph-persistence.ipynb
|
||||
- Add cross-thread persistence: how-tos/cross-thread-persistence.ipynb
|
||||
- Use Postgres checkpointer for persistence: how-tos/persistence_postgres.ipynb
|
||||
- Create custom checkpointer using MongoDB: how-tos/persistence_mongodb.ipynb
|
||||
- Create custom checkpointer using Redis: how-tos/persistence_redis.ipynb
|
||||
- Memory:
|
||||
- Manage conversation history: how-tos/memory/manage-conversation-history.ipynb
|
||||
- Delete messages: how-tos/memory/delete-messages.ipynb
|
||||
- Add summary of the conversation history: how-tos/memory/add-summary-conversation-history.ipynb
|
||||
- Human-in-the-loop:
|
||||
- Add breakpoints: how-tos/human_in_the_loop/breakpoints.ipynb
|
||||
- Add dynamic breakpoints: how-tos/human_in_the_loop/dynamic_breakpoints.ipynb
|
||||
- Wait for user input: how-tos/human_in_the_loop/wait-user-input.ipynb
|
||||
- View and update past graph state: how-tos/human_in_the_loop/time-travel.ipynb
|
||||
- Edit graph state: how-tos/human_in_the_loop/edit-graph-state.ipynb
|
||||
- Review tool calls: how-tos/human_in_the_loop/review-tool-calls.ipynb
|
||||
- Streaming:
|
||||
- Stream full state: how-tos/stream-values.ipynb
|
||||
- Stream state updates: how-tos/stream-updates.ipynb
|
||||
- Stream LLM tokens: how-tos/streaming-tokens.ipynb
|
||||
- Stream LLM tokens without LangChain models: how-tos/streaming-tokens-without-langchain.ipynb
|
||||
- Stream custom data: how-tos/streaming-content.ipynb
|
||||
- Configure multiple streaming modes: how-tos/stream-multiple.ipynb
|
||||
- Stream events from within tools: how-tos/streaming-events-from-within-tools.ipynb
|
||||
- Stream events from within tools without LangChain models: how-tos/streaming-events-from-within-tools-without-langchain.ipynb
|
||||
- Stream events from the final node: how-tos/streaming-from-final-node.ipynb
|
||||
- Stream from subgraphs: how-tos/streaming-subgraphs.ipynb
|
||||
- Disable streaming for models that don't support it: how-tos/disable-streaming.ipynb
|
||||
- Tool calling:
|
||||
- Call tools using ToolNode: how-tos/tool-calling.ipynb
|
||||
- Handle tool calling errors: how-tos/tool-calling-errors.ipynb
|
||||
- Pass runtime values to tools: how-tos/pass-run-time-values-to-tools.ipynb
|
||||
- Pass config to tools: how-tos/pass-config-to-tools.ipynb
|
||||
- Handle many tools: how-tos/many-tools.ipynb
|
||||
- Subgraphs:
|
||||
- Add and use subgraphs: how-tos/subgraph.ipynb
|
||||
- View and update state in subgraphs: how-tos/subgraphs-manage-state.ipynb
|
||||
- Transform inputs and outputs of a subgraph: how-tos/subgraph-transform-state.ipynb
|
||||
- State Management:
|
||||
- Use Pydantic model as state: how-tos/state-model.ipynb
|
||||
- Have a separate input and output schema: how-tos/input_output_schema.ipynb
|
||||
- Pass private state between nodes inside the graph: how-tos/pass_private_state.ipynb
|
||||
- Other:
|
||||
- Run graph asynchronously: how-tos/async.ipynb
|
||||
- Visualize your graph: how-tos/visualization.ipynb
|
||||
- Add runtime configuration: how-tos/configuration.ipynb
|
||||
- Add node retries: how-tos/node-retries.ipynb
|
||||
- Return structured output from a ReAct agent: how-tos/react-agent-structured-output.ipynb
|
||||
- Pass custom LangSmith run ID for graph runs: how-tos/run-id-langsmith.ipynb
|
||||
- Return state before hitting recursion limit: how-tos/return-when-recursion-limit-hits.ipynb
|
||||
- Error reference:
|
||||
- "troubleshooting/errors/index.md"
|
||||
- GRAPH_RECURSION_LIMIT: "troubleshooting/errors/GRAPH_RECURSION_LIMIT.md"
|
||||
- INVALID_CONCURRENT_GRAPH_UPDATE: "troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md"
|
||||
- INVALID_GRAPH_NODE_RETURN_VALUE: "troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md"
|
||||
- MULTIPLE_SUBGRAPHS: "troubleshooting/errors/MULTIPLE_SUBGRAPHS.md"
|
||||
- Prebuilt ReAct Agent:
|
||||
- Create a ReAct agent: how-tos/create-react-agent.ipynb
|
||||
- Add memory to a ReAct agent: how-tos/create-react-agent-memory.ipynb
|
||||
- Add a system prompt to a ReAct agent: how-tos/create-react-agent-system-prompt.ipynb
|
||||
- Add Human-in-the-loop to a ReAct agent: how-tos/create-react-agent-hitl.ipynb
|
||||
- Create prebuilt ReAct agent from scratch: how-tos/react-agent-from-scratch.ipynb
|
||||
- "Conceptual Guides":
|
||||
- Why LangGraph?: concepts/high_level.md
|
||||
- LangGraph Glossary: concepts/low_level.md
|
||||
- Common Agentic Patterns: concepts/agentic_concepts.md
|
||||
- Human-in-the-Loop: concepts/human_in_the_loop.md
|
||||
- Memory: concepts/memory.md
|
||||
- Multi-Agent Systems: concepts/multi_agent.md
|
||||
- Persistence: concepts/persistence.md
|
||||
- Streaming: concepts/streaming.md
|
||||
- FAQ: concepts/faq.md
|
||||
- "tutorials/index.md"
|
||||
- "concepts/index.md"
|
||||
- "how-tos/index.md"
|
||||
- Reference:
|
||||
- "reference/index.md"
|
||||
- Graphs: reference/graphs.md
|
||||
- Checkpointing: reference/checkpoints.md
|
||||
- Storage: reference/store.md
|
||||
@@ -351,7 +235,14 @@ extra:
|
||||
note: >-
|
||||
Thanks for your feedback! Please help us improve this page by adding to the discussion below.
|
||||
validation:
|
||||
omitted_files: warn
|
||||
# https://www.mkdocs.org/user-guide/configuration/
|
||||
# We're `ignoring` nav.omitted_files because we are going to rely
|
||||
# on files being properly links to from the index pages of:
|
||||
# - tutorials
|
||||
# - concepts
|
||||
# - how-tos
|
||||
# - reference
|
||||
omitted_files: ignore
|
||||
absolute_links: warn
|
||||
unrecognized_links: warn
|
||||
# TODO: figure out how to enable 'warn' for this
|
||||
|
||||
Generated
+2
-2
@@ -385,7 +385,7 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -1055,4 +1055,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "d43686e1d06c86dbcc76672790f771e094fd105ea255c86ba8906a2e77e4df03"
|
||||
content-hash = "c319b072af396b6f10fd6f75544816ea717741ed8b35ce675df65506c585f67e"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-duckdb"
|
||||
version = "2.0.0"
|
||||
version = "2.0.1"
|
||||
description = "Library with a DuckDB implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
langgraph-checkpoint = "^2.0.0"
|
||||
langgraph-checkpoint = "^2.0.2"
|
||||
duckdb = ">=1.1.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
Generated
+3
-3
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@@ -324,7 +324,7 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.0"
|
||||
version = "2.0.2"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -1116,4 +1116,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "8f763cd1727287f8c8b5ad2b4d8df00fb446e68d0cd4e88c278e4007969b83fd"
|
||||
content-hash = "6bd85ce8ee1192995c1ff03d5fa65af8ee7872214d71b84559a6192cadf82be6"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
langgraph-checkpoint = "^2.0.0"
|
||||
langgraph-checkpoint = "^2.0.2"
|
||||
orjson = ">=3.10.1"
|
||||
psycopg = "^3.0.0"
|
||||
psycopg-pool = "^3.0.0"
|
||||
|
||||
Generated
+2
-2
@@ -332,7 +332,7 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.0"
|
||||
version = "2.0.2"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -1001,4 +1001,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9.0"
|
||||
content-hash = "e0091cc2deab4de99a6bc4eb262b0040b771a9659dd3638ac1c4a225a1f11dc2"
|
||||
content-hash = "927b49b9ba72a301980237d7adc2e73cdacfbe127a174c7488136a9af9372796"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "2.0.0"
|
||||
version = "2.0.1"
|
||||
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0"
|
||||
langgraph-checkpoint = "^2.0.0"
|
||||
langgraph-checkpoint = "^2.0.2"
|
||||
aiosqlite = "^0.20.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -514,6 +514,14 @@ class CompiledGraph(Pregel):
|
||||
self.nodes[end].triggers.append(channel_name)
|
||||
cast(list[str], self.nodes[end].channels).append(channel_name)
|
||||
|
||||
async def aget_graph(
|
||||
self,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
*,
|
||||
xray: Union[int, bool] = False,
|
||||
) -> DrawableGraph:
|
||||
return self.get_graph(config, xray=xray)
|
||||
|
||||
def get_graph(
|
||||
self,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import json
|
||||
from copy import copy
|
||||
from typing import (
|
||||
@@ -16,6 +17,7 @@ from typing import (
|
||||
Type,
|
||||
Union,
|
||||
cast,
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from langchain_core.messages import (
|
||||
@@ -34,6 +36,7 @@ from langchain_core.tools import BaseTool, InjectedToolArg
|
||||
from langchain_core.tools import tool as create_tool
|
||||
from typing_extensions import Annotated, get_args, get_origin
|
||||
|
||||
from langgraph.errors import GraphInterrupt
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.utils.runnable import RunnableCallable
|
||||
|
||||
@@ -67,13 +70,96 @@ def msg_content_output(output: Any) -> str | List[dict]:
|
||||
return str(output)
|
||||
|
||||
|
||||
def _handle_tool_error(
|
||||
e: Exception,
|
||||
*,
|
||||
flag: Union[
|
||||
bool,
|
||||
str,
|
||||
Callable[..., str],
|
||||
tuple[type[Exception], ...],
|
||||
],
|
||||
) -> str:
|
||||
if isinstance(flag, (bool, tuple)):
|
||||
content = TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e))
|
||||
elif isinstance(flag, str):
|
||||
content = flag
|
||||
elif callable(flag):
|
||||
content = flag(e)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Got unexpected type of `handle_tool_error`. Expected bool, str "
|
||||
f"or callable. Received: {flag}"
|
||||
)
|
||||
return content
|
||||
|
||||
|
||||
def _infer_handled_types(handler: Callable[..., str]) -> tuple[type[Exception]]:
|
||||
sig = inspect.signature(handler)
|
||||
params = list(sig.parameters.values())
|
||||
if params:
|
||||
# If it's a method, the first argument is typically 'self' or 'cls'
|
||||
if params[0].name in ["self", "cls"] and len(params) == 2:
|
||||
first_param = params[1]
|
||||
else:
|
||||
first_param = params[0]
|
||||
|
||||
type_hints = get_type_hints(handler)
|
||||
if first_param.name in type_hints:
|
||||
origin = get_origin(first_param.annotation)
|
||||
if origin is Union:
|
||||
args = get_args(first_param.annotation)
|
||||
if all(issubclass(arg, Exception) for arg in args):
|
||||
return tuple(args)
|
||||
else:
|
||||
raise ValueError(
|
||||
"All types in the error handler error annotation must be Exception types. "
|
||||
"For example, `def custom_handler(e: Union[ValueError, TypeError])`. "
|
||||
f"Got '{first_param.annotation}' instead."
|
||||
)
|
||||
|
||||
exception_type = type_hints[first_param.name]
|
||||
if Exception in exception_type.__mro__:
|
||||
return (exception_type,)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Arbitrary types are not supported in the error handler signature. "
|
||||
"Please annotate the error with either a specific Exception type or a union of Exception types. "
|
||||
"For example, `def custom_handler(e: ValueError)` or `def custom_handler(e: Union[ValueError, TypeError])`. "
|
||||
f"Got '{exception_type}' instead."
|
||||
)
|
||||
|
||||
# If no type information is available, return (Exception,) for backwards compatibility.
|
||||
return (Exception,)
|
||||
|
||||
|
||||
class ToolNode(RunnableCallable):
|
||||
"""A node that runs the tools called in the last AIMessage.
|
||||
|
||||
It can be used either in StateGraph with a "messages" key (or a custom key passed via ToolNode's 'messages_key').
|
||||
It can be used either in StateGraph with a "messages" state key (or a custom key passed via ToolNode's 'messages_key').
|
||||
If multiple tool calls are requested, they will be run in parallel. The output will be
|
||||
a list of ToolMessages, one for each tool call.
|
||||
|
||||
Args:
|
||||
tools: A sequence of tools that can be invoked by the ToolNode.
|
||||
name: The name of the ToolNode in the graph. Defaults to "tools".
|
||||
tags: Optional tags to associate with the node. Defaults to None.
|
||||
handle_tool_errors: How to handle tool errors raised by tools inside the node. Defaults to True.
|
||||
Must be one of the following:
|
||||
|
||||
- True: all errors will be caught and
|
||||
a ToolMessage with a default error message (TOOL_CALL_ERROR_TEMPLATE) will be returned.
|
||||
- str: all errors will be caught and
|
||||
a ToolMessage with the string value of 'handle_tool_errors' will be returned.
|
||||
- tuple[type[Exception], ...]: exceptions in the tuple will be caught and
|
||||
a ToolMessage with a default error message (TOOL_CALL_ERROR_TEMPLATE) will be returned.
|
||||
- Callable[..., str]: exceptions from the signature of the callable will be caught and
|
||||
a ToolMessage with the string value of the result of the 'handle_tool_errors' callable will be returned.
|
||||
- False: none of the errors raised by the tools will be caught
|
||||
messages_key: The state key in the input that contains the list of messages.
|
||||
The same key will be used for the output from the ToolNode.
|
||||
Defaults to "messages".
|
||||
|
||||
The `ToolNode` is roughly analogous to:
|
||||
|
||||
```python
|
||||
@@ -101,7 +187,9 @@ class ToolNode(RunnableCallable):
|
||||
*,
|
||||
name: str = "tools",
|
||||
tags: Optional[list[str]] = None,
|
||||
handle_tool_errors: Optional[bool] = True,
|
||||
handle_tool_errors: Union[
|
||||
bool, str, Callable[..., str], tuple[type[Exception], ...]
|
||||
] = True,
|
||||
messages_key: str = "messages",
|
||||
) -> None:
|
||||
super().__init__(self._func, self._afunc, name=name, tags=tags, trace=False)
|
||||
@@ -180,15 +268,38 @@ class ToolNode(RunnableCallable):
|
||||
Union[str, list], msg_content_output(tool_message.content)
|
||||
)
|
||||
return tool_message
|
||||
# GraphInterrupt is a special exception that will always be raised.
|
||||
# It can be triggered in the following scenarios:
|
||||
# (1) a NodeInterrupt is raised inside a tool
|
||||
# (2) a NodeInterrupt is raised inside a graph node for a graph called as a tool
|
||||
# (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph called as a tool
|
||||
# (2 and 3 can happen in a "supervisor w/ tools" multi-agent architecture)
|
||||
except GraphInterrupt as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
if not self.handle_tool_errors:
|
||||
if isinstance(self.handle_tool_errors, tuple):
|
||||
handled_types: tuple = self.handle_tool_errors
|
||||
elif callable(self.handle_tool_errors):
|
||||
handled_types = _infer_handled_types(self.handle_tool_errors)
|
||||
else:
|
||||
# default behavior is catching all exceptions
|
||||
handled_types = (Exception,)
|
||||
|
||||
# Unhandled
|
||||
if not self.handle_tool_errors or not isinstance(e, handled_types):
|
||||
raise e
|
||||
content = TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e))
|
||||
return ToolMessage(content, name=call["name"], tool_call_id=call["id"])
|
||||
# Handled
|
||||
else:
|
||||
content = _handle_tool_error(e, flag=self.handle_tool_errors)
|
||||
|
||||
return ToolMessage(
|
||||
content=content, name=call["name"], tool_call_id=call["id"], status="error"
|
||||
)
|
||||
|
||||
async def _arun_one(self, call: ToolCall, config: RunnableConfig) -> ToolMessage:
|
||||
if invalid_tool_message := self._validate_tool_call(call):
|
||||
return invalid_tool_message
|
||||
|
||||
try:
|
||||
input = {**call, **{"type": "tool_call"}}
|
||||
tool_message: ToolMessage = await self.tools_by_name[call["name"]].ainvoke(
|
||||
@@ -198,11 +309,33 @@ class ToolNode(RunnableCallable):
|
||||
Union[str, list], msg_content_output(tool_message.content)
|
||||
)
|
||||
return tool_message
|
||||
# GraphInterrupt is a special exception that will always be raised.
|
||||
# It can be triggered in the following scenarios:
|
||||
# (1) a NodeInterrupt is raised inside a tool
|
||||
# (2) a NodeInterrupt is raised inside a graph node for a graph called as a tool
|
||||
# (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph called as a tool
|
||||
# (2 and 3 can happen in a "supervisor w/ tools" multi-agent architecture)
|
||||
except GraphInterrupt as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
if not self.handle_tool_errors:
|
||||
if isinstance(self.handle_tool_errors, tuple):
|
||||
handled_types: tuple = self.handle_tool_errors
|
||||
elif callable(self.handle_tool_errors):
|
||||
handled_types = _infer_handled_types(self.handle_tool_errors)
|
||||
else:
|
||||
# default behavior is catching all exceptions
|
||||
handled_types = (Exception,)
|
||||
|
||||
# Unhandled
|
||||
if not self.handle_tool_errors or not isinstance(e, handled_types):
|
||||
raise e
|
||||
content = TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e))
|
||||
return ToolMessage(content, name=call["name"], tool_call_id=call["id"])
|
||||
# Handled
|
||||
else:
|
||||
content = _handle_tool_error(e, flag=self.handle_tool_errors)
|
||||
|
||||
return ToolMessage(
|
||||
content=content, name=call["name"], tool_call_id=call["id"], status="error"
|
||||
)
|
||||
|
||||
def _parse_input(
|
||||
self,
|
||||
@@ -240,7 +373,9 @@ class ToolNode(RunnableCallable):
|
||||
requested_tool=requested_tool,
|
||||
available_tools=", ".join(self.tools_by_name.keys()),
|
||||
)
|
||||
return ToolMessage(content, name=requested_tool, tool_call_id=call["id"])
|
||||
return ToolMessage(
|
||||
content, name=requested_tool, tool_call_id=call["id"], status="error"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ from uuid import UUID, uuid5
|
||||
|
||||
from langchain_core.globals import get_debug
|
||||
from langchain_core.runnables import (
|
||||
Runnable,
|
||||
RunnableSequence,
|
||||
)
|
||||
from langchain_core.runnables.base import Input, Output
|
||||
@@ -34,6 +33,7 @@ from langchain_core.runnables.config import (
|
||||
get_async_callback_manager_for_config,
|
||||
get_callback_manager_for_config,
|
||||
)
|
||||
from langchain_core.runnables.graph import Graph
|
||||
from langchain_core.runnables.utils import (
|
||||
ConfigurableFieldSpec,
|
||||
get_unique_config_specs,
|
||||
@@ -86,6 +86,7 @@ from langgraph.pregel.io import read_channels
|
||||
from langgraph.pregel.loop import AsyncPregelLoop, StreamProtocol, SyncPregelLoop
|
||||
from langgraph.pregel.manager import AsyncChannelsManager, ChannelsManager
|
||||
from langgraph.pregel.messages import StreamMessagesHandler
|
||||
from langgraph.pregel.protocol import PregelProtocol
|
||||
from langgraph.pregel.read import PregelNode
|
||||
from langgraph.pregel.retry import RetryPolicy
|
||||
from langgraph.pregel.runner import PregelRunner
|
||||
@@ -179,7 +180,7 @@ class Channel:
|
||||
)
|
||||
|
||||
|
||||
class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]):
|
||||
class Pregel(PregelProtocol):
|
||||
nodes: dict[str, PregelNode]
|
||||
|
||||
channels: dict[str, Union[BaseChannel, ManagedValueSpec]]
|
||||
@@ -259,6 +260,16 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]):
|
||||
if auto_validate:
|
||||
self.validate()
|
||||
|
||||
def get_graph(
|
||||
self, config: RunnableConfig | None = None, *, xray: int | bool = False
|
||||
) -> Graph:
|
||||
raise NotImplementedError
|
||||
|
||||
async def aget_graph(
|
||||
self, config: RunnableConfig | None = None, *, xray: int | bool = False
|
||||
) -> Graph:
|
||||
raise NotImplementedError
|
||||
|
||||
def copy(self, update: dict[str, Any] | None = None) -> Self:
|
||||
attrs = {**self.__dict__, **(update or {})}
|
||||
return self.__class__(**attrs)
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Iterator,
|
||||
Optional,
|
||||
Protocol,
|
||||
Sequence,
|
||||
Union,
|
||||
runtime_checkable,
|
||||
)
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from langchain_core.runnables.graph import Graph as DrawableGraph
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.pregel.types import All, StateSnapshot, StreamMode
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class PregelProtocol(Protocol):
|
||||
class PregelProtocol(
|
||||
Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]], ABC
|
||||
):
|
||||
@abstractmethod
|
||||
def with_config(
|
||||
self, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Self: ...
|
||||
|
||||
@abstractmethod
|
||||
def get_graph(
|
||||
self,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
@@ -29,6 +31,7 @@ class PregelProtocol(Protocol):
|
||||
xray: Union[int, bool] = False,
|
||||
) -> DrawableGraph: ...
|
||||
|
||||
@abstractmethod
|
||||
async def aget_graph(
|
||||
self,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
@@ -36,22 +39,17 @@ class PregelProtocol(Protocol):
|
||||
xray: Union[int, bool] = False,
|
||||
) -> DrawableGraph: ...
|
||||
|
||||
def get_subgraphs(
|
||||
self, namespace: Optional[str] = None, recurse: bool = False
|
||||
) -> Iterator[tuple[str, "PregelProtocol"]]: ...
|
||||
|
||||
def aget_subgraphs(
|
||||
self, namespace: Optional[str] = None, recurse: bool = False
|
||||
) -> AsyncIterator[tuple[str, "PregelProtocol"]]: ...
|
||||
|
||||
@abstractmethod
|
||||
def get_state(
|
||||
self, config: RunnableConfig, *, subgraphs: bool = False
|
||||
) -> StateSnapshot: ...
|
||||
|
||||
@abstractmethod
|
||||
async def aget_state(
|
||||
self, config: RunnableConfig, *, subgraphs: bool = False
|
||||
) -> StateSnapshot: ...
|
||||
|
||||
@abstractmethod
|
||||
def get_state_history(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
@@ -61,6 +59,7 @@ class PregelProtocol(Protocol):
|
||||
limit: Optional[int] = None,
|
||||
) -> Iterator[StateSnapshot]: ...
|
||||
|
||||
@abstractmethod
|
||||
def aget_state_history(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
@@ -70,6 +69,7 @@ class PregelProtocol(Protocol):
|
||||
limit: Optional[int] = None,
|
||||
) -> AsyncIterator[StateSnapshot]: ...
|
||||
|
||||
@abstractmethod
|
||||
def update_state(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
@@ -77,6 +77,7 @@ class PregelProtocol(Protocol):
|
||||
as_node: Optional[str] = None,
|
||||
) -> RunnableConfig: ...
|
||||
|
||||
@abstractmethod
|
||||
async def aupdate_state(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
@@ -84,6 +85,7 @@ class PregelProtocol(Protocol):
|
||||
as_node: Optional[str] = None,
|
||||
) -> RunnableConfig: ...
|
||||
|
||||
@abstractmethod
|
||||
def stream(
|
||||
self,
|
||||
input: Union[dict[str, Any], Any],
|
||||
@@ -95,6 +97,7 @@ class PregelProtocol(Protocol):
|
||||
subgraphs: bool = False,
|
||||
) -> Iterator[Union[dict[str, Any], Any]]: ...
|
||||
|
||||
@abstractmethod
|
||||
def astream(
|
||||
self,
|
||||
input: Union[dict[str, Any], Any],
|
||||
@@ -106,6 +109,7 @@ class PregelProtocol(Protocol):
|
||||
subgraphs: bool = False,
|
||||
) -> AsyncIterator[Union[dict[str, Any], Any]]: ...
|
||||
|
||||
@abstractmethod
|
||||
def invoke(
|
||||
self,
|
||||
input: Union[dict[str, Any], Any],
|
||||
@@ -115,6 +119,7 @@ class PregelProtocol(Protocol):
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
||||
) -> Union[dict[str, Any], Any]: ...
|
||||
|
||||
@abstractmethod
|
||||
async def ainvoke(
|
||||
self,
|
||||
input: Union[dict[str, Any], Any],
|
||||
|
||||
@@ -2,6 +2,7 @@ from typing import (
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Iterator,
|
||||
Literal,
|
||||
Optional,
|
||||
Sequence,
|
||||
Union,
|
||||
@@ -9,7 +10,7 @@ from typing import (
|
||||
)
|
||||
|
||||
import orjson
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.runnables.graph import (
|
||||
Edge as DrawableEdge,
|
||||
)
|
||||
@@ -44,10 +45,14 @@ class RemoteException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RemoteGraph(PregelProtocol, Runnable):
|
||||
class RemoteGraph(PregelProtocol):
|
||||
name: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
graph_id: str,
|
||||
name: str, # graph_id
|
||||
/,
|
||||
*,
|
||||
url: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
headers: Optional[dict[str, str]] = None,
|
||||
@@ -60,7 +65,7 @@ class RemoteGraph(PregelProtocol, Runnable):
|
||||
If `client` or `sync_client` are provided, they will be used instead of the default clients.
|
||||
See `LangGraphClient` and `SyncLangGraphClient` for details on the default clients.
|
||||
"""
|
||||
self.graph_id = graph_id
|
||||
self.name = name
|
||||
self.config = config
|
||||
self.client = client or get_client(url=url, api_key=api_key, headers=headers)
|
||||
self.sync_client = sync_client or get_sync_client(
|
||||
@@ -69,7 +74,7 @@ class RemoteGraph(PregelProtocol, Runnable):
|
||||
|
||||
def copy(self, update: dict[str, Any]) -> Self:
|
||||
attrs = {**self.__dict__, **update}
|
||||
return self.__class__(**attrs)
|
||||
return self.__class__(attrs.pop("name"), **attrs)
|
||||
|
||||
def with_config(
|
||||
self, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
@@ -99,7 +104,7 @@ class RemoteGraph(PregelProtocol, Runnable):
|
||||
xray: Union[int, bool] = False,
|
||||
) -> DrawableGraph:
|
||||
graph = self.sync_client.assistants.get_graph(
|
||||
assistant_id=self.graph_id,
|
||||
assistant_id=self.name,
|
||||
xray=xray,
|
||||
)
|
||||
return DrawableGraph(
|
||||
@@ -114,7 +119,7 @@ class RemoteGraph(PregelProtocol, Runnable):
|
||||
xray: Union[int, bool] = False,
|
||||
) -> DrawableGraph:
|
||||
graph = await self.client.assistants.get_graph(
|
||||
assistant_id=self.graph_id,
|
||||
assistant_id=self.name,
|
||||
xray=xray,
|
||||
)
|
||||
return DrawableGraph(
|
||||
@@ -122,30 +127,6 @@ class RemoteGraph(PregelProtocol, Runnable):
|
||||
edges=[DrawableEdge(**edge) for edge in graph["edges"]],
|
||||
)
|
||||
|
||||
def get_subgraphs(
|
||||
self, namespace: Optional[str] = None, recurse: bool = False
|
||||
) -> Iterator[tuple[str, "PregelProtocol"]]:
|
||||
subgraphs = self.sync_client.assistants.get_subgraphs(
|
||||
assistant_id=self.graph_id,
|
||||
namespace=namespace,
|
||||
recurse=recurse,
|
||||
)
|
||||
for namespace, graph_schema in subgraphs.items():
|
||||
remote_subgraph = self.copy({"graph_id": graph_schema["graph_id"]})
|
||||
yield (namespace, remote_subgraph)
|
||||
|
||||
async def aget_subgraphs(
|
||||
self, namespace: Optional[str] = None, recurse: bool = False
|
||||
) -> AsyncIterator[tuple[str, "PregelProtocol"]]:
|
||||
subgraphs = await self.client.assistants.get_subgraphs(
|
||||
assistant_id=self.graph_id,
|
||||
namespace=namespace,
|
||||
recurse=recurse,
|
||||
)
|
||||
for namespace, graph_schema in subgraphs.items():
|
||||
remote_subgraph = self.copy({"graph_id": graph_schema["graph_id"]})
|
||||
yield (namespace, remote_subgraph)
|
||||
|
||||
def _create_state_snapshot(self, state: ThreadState) -> StateSnapshot:
|
||||
tasks = []
|
||||
for task in state["tasks"]:
|
||||
@@ -258,7 +239,11 @@ class RemoteGraph(PregelProtocol, Runnable):
|
||||
if k not in reserved_configurable_keys and not k.startswith("__pregel_")
|
||||
}
|
||||
|
||||
return {"configurable": new_configurable}
|
||||
return {
|
||||
"tags": config.get("tags") or [],
|
||||
"metadata": config.get("metadata") or {},
|
||||
"configurable": new_configurable,
|
||||
}
|
||||
|
||||
def get_state(
|
||||
self, config: RunnableConfig, *, subgraphs: bool = False
|
||||
@@ -402,8 +387,8 @@ class RemoteGraph(PregelProtocol, Runnable):
|
||||
stream_modes, req_updates, req_single = self._get_stream_modes(stream_mode)
|
||||
|
||||
for chunk in self.sync_client.runs.stream(
|
||||
thread_id=cast(str, sanitized_config["configurable"]["thread_id"]),
|
||||
assistant_id=self.graph_id,
|
||||
thread_id=sanitized_config["configurable"].get("thread_id"),
|
||||
assistant_id=self.name,
|
||||
input=input,
|
||||
config=sanitized_config,
|
||||
stream_mode=stream_modes,
|
||||
@@ -449,8 +434,8 @@ class RemoteGraph(PregelProtocol, Runnable):
|
||||
stream_modes, req_updates, req_single = self._get_stream_modes(stream_mode)
|
||||
|
||||
async for chunk in self.client.runs.stream(
|
||||
thread_id=sanitized_config["configurable"]["thread_id"],
|
||||
assistant_id=self.graph_id,
|
||||
thread_id=sanitized_config["configurable"].get("thread_id"),
|
||||
assistant_id=self.name,
|
||||
input=input,
|
||||
config=sanitized_config,
|
||||
stream_mode=stream_modes,
|
||||
@@ -481,6 +466,22 @@ class RemoteGraph(PregelProtocol, Runnable):
|
||||
else:
|
||||
yield chunk
|
||||
|
||||
async def astream_events(
|
||||
self,
|
||||
input: Any,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
*,
|
||||
version: Literal["v1", "v2"],
|
||||
include_names: Optional[Sequence[All]] = None,
|
||||
include_types: Optional[Sequence[All]] = None,
|
||||
include_tags: Optional[Sequence[All]] = None,
|
||||
exclude_names: Optional[Sequence[All]] = None,
|
||||
exclude_types: Optional[Sequence[All]] = None,
|
||||
exclude_tags: Optional[Sequence[All]] = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[dict[str, Any]]:
|
||||
raise NotImplementedError
|
||||
|
||||
def invoke(
|
||||
self,
|
||||
input: Union[dict[str, Any], Any],
|
||||
@@ -493,8 +494,8 @@ class RemoteGraph(PregelProtocol, Runnable):
|
||||
sanitized_config = self._sanitize_config(merged_config)
|
||||
|
||||
return self.sync_client.runs.wait(
|
||||
thread_id=sanitized_config["configurable"]["thread_id"],
|
||||
assistant_id=self.graph_id,
|
||||
thread_id=sanitized_config["configurable"].get("thread_id"),
|
||||
assistant_id=self.name,
|
||||
input=input,
|
||||
config=sanitized_config,
|
||||
interrupt_before=interrupt_before,
|
||||
@@ -514,8 +515,8 @@ class RemoteGraph(PregelProtocol, Runnable):
|
||||
sanitized_config = self._sanitize_config(merged_config)
|
||||
|
||||
return await self.client.runs.wait(
|
||||
thread_id=sanitized_config["configurable"]["thread_id"],
|
||||
assistant_id=self.graph_id,
|
||||
thread_id=sanitized_config["configurable"].get("thread_id"),
|
||||
assistant_id=self.name,
|
||||
input=input,
|
||||
config=sanitized_config,
|
||||
interrupt_before=interrupt_before,
|
||||
|
||||
@@ -4,6 +4,7 @@ from langchain_core.runnables import RunnableLambda, RunnableSequence
|
||||
from langchain_core.runnables.utils import get_function_nonlocals
|
||||
|
||||
from langgraph.checkpoint.base import ChannelVersions
|
||||
from langgraph.pregel.protocol import PregelProtocol
|
||||
from langgraph.utils.runnable import Runnable, RunnableCallable, RunnableSeq
|
||||
|
||||
|
||||
@@ -32,9 +33,9 @@ def find_subgraph_pregel(candidate: Runnable) -> Optional[Runnable]:
|
||||
|
||||
for c in candidates:
|
||||
if (
|
||||
isinstance(c, Pregel)
|
||||
isinstance(c, PregelProtocol)
|
||||
# subgraphs that disabled checkpointing are not considered
|
||||
and c.checkpointer is not False
|
||||
and (not isinstance(c, Pregel) or c.checkpointer is not False)
|
||||
):
|
||||
return c
|
||||
elif isinstance(c, RunnableSequence) or isinstance(c, RunnableSeq):
|
||||
|
||||
@@ -29,13 +29,16 @@ from langchain_core.messages import (
|
||||
)
|
||||
from langchain_core.outputs import ChatGeneration, ChatResult
|
||||
from langchain_core.runnables import Runnable, RunnableLambda
|
||||
from langchain_core.tools import BaseTool
|
||||
from langchain_core.tools import BaseTool, ToolException
|
||||
from langchain_core.tools import tool as dec_tool
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from pydantic.v1 import ValidationError as ValidationErrorV1
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.errors import NodeInterrupt
|
||||
from langgraph.graph import START, MessagesState, StateGraph, add_messages
|
||||
from langgraph.prebuilt import (
|
||||
ToolNode,
|
||||
@@ -43,9 +46,15 @@ from langgraph.prebuilt import (
|
||||
create_react_agent,
|
||||
tools_condition,
|
||||
)
|
||||
from langgraph.prebuilt.tool_node import InjectedState, InjectedStore
|
||||
from langgraph.prebuilt.tool_node import (
|
||||
TOOL_CALL_ERROR_TEMPLATE,
|
||||
InjectedState,
|
||||
InjectedStore,
|
||||
_infer_handled_types,
|
||||
)
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
from langgraph.types import Interrupt
|
||||
from tests.conftest import (
|
||||
ALL_CHECKPOINTERS_ASYNC,
|
||||
ALL_CHECKPOINTERS_SYNC,
|
||||
@@ -368,32 +377,107 @@ def test_model_with_tools(tool_style: str):
|
||||
create_react_agent(model.bind_tools([tool1]), [tool2])
|
||||
|
||||
|
||||
def test__infer_handled_types() -> None:
|
||||
def handle(e): # type: ignore
|
||||
return ""
|
||||
|
||||
def handle2(e: Exception) -> str:
|
||||
return ""
|
||||
|
||||
def handle3(e: Union[ValueError, ToolException]) -> str:
|
||||
return ""
|
||||
|
||||
class Handler:
|
||||
def handle(self, e: ValueError) -> str:
|
||||
return ""
|
||||
|
||||
handle4 = Handler().handle
|
||||
|
||||
def handle5(e: Union[Union[TypeError, ValueError], ToolException]):
|
||||
return ""
|
||||
|
||||
expected: tuple = (Exception,)
|
||||
actual = _infer_handled_types(handle)
|
||||
assert expected == actual
|
||||
|
||||
expected = (Exception,)
|
||||
actual = _infer_handled_types(handle2)
|
||||
assert expected == actual
|
||||
|
||||
expected = (ValueError, ToolException)
|
||||
actual = _infer_handled_types(handle3)
|
||||
assert expected == actual
|
||||
|
||||
expected = (ValueError,)
|
||||
actual = _infer_handled_types(handle4)
|
||||
assert expected == actual
|
||||
|
||||
expected = (TypeError, ValueError, ToolException)
|
||||
actual = _infer_handled_types(handle5)
|
||||
assert expected == actual
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
def handler(e: str):
|
||||
return ""
|
||||
|
||||
_infer_handled_types(handler)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
def handler(e: list[Exception]):
|
||||
return ""
|
||||
|
||||
_infer_handled_types(handler)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
|
||||
def handler(e: Union[str, int]):
|
||||
return ""
|
||||
|
||||
_infer_handled_types(handler)
|
||||
|
||||
|
||||
# tools for testing Too
|
||||
def tool1(some_val: int, some_other_val: str) -> str:
|
||||
"""Tool 1 docstring."""
|
||||
if some_val == 0:
|
||||
raise ValueError("Test error")
|
||||
return f"{some_val} - {some_other_val}"
|
||||
|
||||
|
||||
async def tool2(some_val: int, some_other_val: str) -> str:
|
||||
"""Tool 2 docstring."""
|
||||
if some_val == 0:
|
||||
raise ToolException("Test error")
|
||||
return f"tool2: {some_val} - {some_other_val}"
|
||||
|
||||
|
||||
async def tool3(some_val: int, some_other_val: str) -> str:
|
||||
"""Tool 3 docstring."""
|
||||
return [
|
||||
{"key_1": some_val, "key_2": "foo"},
|
||||
{"key_1": some_other_val, "key_2": "baz"},
|
||||
]
|
||||
|
||||
|
||||
async def tool4(some_val: int, some_other_val: str) -> str:
|
||||
"""Tool 4 docstring."""
|
||||
return [
|
||||
{"type": "image_url", "image_url": {"url": "abdc"}},
|
||||
]
|
||||
|
||||
|
||||
@dec_tool
|
||||
def tool5(some_val: int):
|
||||
"""Tool 5 docstring."""
|
||||
raise ToolException("Test error")
|
||||
|
||||
|
||||
tool5.handle_tool_error = "foo"
|
||||
|
||||
|
||||
async def test_tool_node():
|
||||
def tool1(some_val: int, some_other_val: str) -> str:
|
||||
"""Tool 1 docstring."""
|
||||
if some_val == 0:
|
||||
raise ValueError("Test error")
|
||||
return f"{some_val} - {some_other_val}"
|
||||
|
||||
async def tool2(some_val: int, some_other_val: str) -> str:
|
||||
"""Tool 2 docstring."""
|
||||
if some_val == 0:
|
||||
raise ValueError("Test error")
|
||||
return f"tool2: {some_val} - {some_other_val}"
|
||||
|
||||
async def tool3(some_val: int, some_other_val: str) -> str:
|
||||
"""Tool 3 docstring."""
|
||||
return [
|
||||
{"key_1": some_val, "key_2": "foo"},
|
||||
{"key_1": some_other_val, "key_2": "baz"},
|
||||
]
|
||||
|
||||
async def tool4(some_val: int, some_other_val: str) -> str:
|
||||
"""Tool 4 docstring."""
|
||||
return [
|
||||
{"type": "image_url", "image_url": {"url": "abdc"}},
|
||||
]
|
||||
|
||||
result = ToolNode([tool1]).invoke(
|
||||
{
|
||||
"messages": [
|
||||
@@ -416,31 +500,6 @@ async def test_tool_node():
|
||||
assert tool_message.content == "1 - foo"
|
||||
assert tool_message.tool_call_id == "some 0"
|
||||
|
||||
result_error = ToolNode([tool1]).invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool1",
|
||||
"args": {"some_val": 0, "some_other_val": "foo"},
|
||||
"id": "some 0",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
tool_message: ToolMessage = result_error["messages"][-1]
|
||||
assert tool_message.type == "tool"
|
||||
assert (
|
||||
tool_message.content
|
||||
== f"Error: {repr(ValueError('Test error'))}\n Please fix your mistakes."
|
||||
)
|
||||
assert tool_message.tool_call_id == "some 0"
|
||||
|
||||
result2 = await ToolNode([tool2]).ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
@@ -457,11 +516,232 @@ async def test_tool_node():
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
tool_message: ToolMessage = result2["messages"][-1]
|
||||
assert tool_message.type == "tool"
|
||||
assert tool_message.content == "tool2: 2 - bar"
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
# list of dicts tool content
|
||||
result3 = await ToolNode([tool3]).ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool3",
|
||||
"args": {"some_val": 2, "some_other_val": "bar"},
|
||||
"id": "some 2",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
tool_message: ToolMessage = result3["messages"][-1]
|
||||
assert tool_message.type == "tool"
|
||||
assert (
|
||||
tool_message.content
|
||||
== '[{"key_1": 2, "key_2": "foo"}, {"key_1": "bar", "key_2": "baz"}]'
|
||||
)
|
||||
assert tool_message.tool_call_id == "some 2"
|
||||
|
||||
# list of content blocks tool content
|
||||
result4 = await ToolNode([tool4]).ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool4",
|
||||
"args": {"some_val": 2, "some_other_val": "bar"},
|
||||
"id": "some 3",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
tool_message: ToolMessage = result4["messages"][-1]
|
||||
assert tool_message.type == "tool"
|
||||
assert tool_message.content == [{"type": "image_url", "image_url": {"url": "abdc"}}]
|
||||
assert tool_message.tool_call_id == "some 3"
|
||||
|
||||
|
||||
async def test_tool_node_error_handling():
|
||||
def handle_all(e: Union[ValueError, ToolException, ValidationError]):
|
||||
return TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e))
|
||||
|
||||
# test catching all exceptions, via:
|
||||
# - handle_tool_errors = True
|
||||
# - passing a tuple of all exceptions
|
||||
# - passing a callable with all exceptions in the signature
|
||||
for handle_tool_errors in (
|
||||
True,
|
||||
(ValueError, ToolException, ValidationError),
|
||||
handle_all,
|
||||
):
|
||||
result_error = await ToolNode(
|
||||
[tool1, tool2, tool3], handle_tool_errors=handle_tool_errors
|
||||
).ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool1",
|
||||
"args": {"some_val": 0, "some_other_val": "foo"},
|
||||
"id": "some id",
|
||||
},
|
||||
{
|
||||
"name": "tool2",
|
||||
"args": {"some_val": 0, "some_other_val": "bar"},
|
||||
"id": "some other id",
|
||||
},
|
||||
{
|
||||
"name": "tool3",
|
||||
"args": {"some_val": 0},
|
||||
"id": "another id",
|
||||
},
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
assert all(m.type == "tool" for m in result_error["messages"])
|
||||
assert all(m.status == "error" for m in result_error["messages"])
|
||||
assert (
|
||||
result_error["messages"][0].content
|
||||
== f"Error: {repr(ValueError('Test error'))}\n Please fix your mistakes."
|
||||
)
|
||||
assert (
|
||||
result_error["messages"][1].content
|
||||
== f"Error: {repr(ToolException('Test error'))}\n Please fix your mistakes."
|
||||
)
|
||||
assert (
|
||||
"ValidationError" in result_error["messages"][2].content
|
||||
or "validation error" in result_error["messages"][2].content
|
||||
)
|
||||
|
||||
assert result_error["messages"][0].tool_call_id == "some id"
|
||||
assert result_error["messages"][1].tool_call_id == "some other id"
|
||||
assert result_error["messages"][2].tool_call_id == "another id"
|
||||
|
||||
|
||||
async def test_tool_node_error_handling_callable():
|
||||
def handle_value_error(e: ValueError):
|
||||
return "Value error"
|
||||
|
||||
def handle_tool_exception(e: ToolException):
|
||||
return "Tool exception"
|
||||
|
||||
for handle_tool_errors in ("Value error", handle_value_error):
|
||||
result_error = await ToolNode(
|
||||
[tool1], handle_tool_errors=handle_tool_errors
|
||||
).ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool1",
|
||||
"args": {"some_val": 0, "some_other_val": "foo"},
|
||||
"id": "some id",
|
||||
},
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
tool_message: ToolMessage = result_error["messages"][-1]
|
||||
assert tool_message.type == "tool"
|
||||
assert tool_message.status == "error"
|
||||
assert tool_message.content == "Value error"
|
||||
|
||||
# test raising for an unhandled exception, via:
|
||||
# - passing a tuple of all exceptions
|
||||
# - passing a callable with all exceptions in the signature
|
||||
for handle_tool_errors in ((ValueError,), handle_value_error):
|
||||
with pytest.raises(ToolException) as exc_info:
|
||||
await ToolNode(
|
||||
[tool1, tool2], handle_tool_errors=handle_tool_errors
|
||||
).ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool1",
|
||||
"args": {"some_val": 0, "some_other_val": "foo"},
|
||||
"id": "some id",
|
||||
},
|
||||
{
|
||||
"name": "tool2",
|
||||
"args": {"some_val": 0, "some_other_val": "bar"},
|
||||
"id": "some other id",
|
||||
},
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
assert str(exc_info.value) == "Test error"
|
||||
|
||||
for handle_tool_errors in ((ToolException,), handle_tool_exception):
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await ToolNode(
|
||||
[tool1, tool2], handle_tool_errors=handle_tool_errors
|
||||
).ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool1",
|
||||
"args": {"some_val": 0, "some_other_val": "foo"},
|
||||
"id": "some id",
|
||||
},
|
||||
{
|
||||
"name": "tool2",
|
||||
"args": {"some_val": 0, "some_other_val": "bar"},
|
||||
"id": "some other id",
|
||||
},
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
assert str(exc_info.value) == "Test error"
|
||||
|
||||
|
||||
async def test_tool_node_handle_tool_errors_false():
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
ToolNode([tool1], handle_tool_errors=False).invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool1",
|
||||
"args": {"some_val": 0, "some_other_val": "foo"},
|
||||
"id": "some id",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
assert str(exc_info.value) == "Test error"
|
||||
|
||||
with pytest.raises(ToolException):
|
||||
await ToolNode([tool2], handle_tool_errors=False).ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
@@ -471,7 +751,7 @@ async def test_tool_node():
|
||||
{
|
||||
"name": "tool2",
|
||||
"args": {"some_val": 0, "some_other_val": "bar"},
|
||||
"id": "some 1",
|
||||
"id": "some id",
|
||||
}
|
||||
],
|
||||
)
|
||||
@@ -479,7 +759,57 @@ async def test_tool_node():
|
||||
}
|
||||
)
|
||||
|
||||
# incorrect tool name
|
||||
assert str(exc_info.value) == "Test error"
|
||||
|
||||
# test validation errors get raised if handle_tool_errors is False
|
||||
with pytest.raises((ValidationError, ValidationErrorV1)):
|
||||
ToolNode([tool1], handle_tool_errors=False).invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool1",
|
||||
"args": {"some_val": 0},
|
||||
"id": "some id",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_tool_node_individual_tool_error_handling():
|
||||
# test error handling on individual tools (and that it overrides overall error handling!)
|
||||
result_individual_tool_error_handler = ToolNode(
|
||||
[tool5], handle_tool_errors="bar"
|
||||
).invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool5",
|
||||
"args": {"some_val": 0},
|
||||
"id": "some 0",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
tool_message: ToolMessage = result_individual_tool_error_handler["messages"][-1]
|
||||
assert tool_message.type == "tool"
|
||||
assert tool_message.status == "error"
|
||||
assert tool_message.content == "foo"
|
||||
assert tool_message.tool_call_id == "some 0"
|
||||
|
||||
|
||||
def test_tool_node_incorrect_tool_name():
|
||||
result_incorrect_name = ToolNode([tool1, tool2]).invoke(
|
||||
{
|
||||
"messages": [
|
||||
@@ -496,60 +826,94 @@ async def test_tool_node():
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
tool_message: ToolMessage = result_incorrect_name["messages"][-1]
|
||||
assert tool_message.type == "tool"
|
||||
assert tool_message.status == "error"
|
||||
assert (
|
||||
tool_message.content
|
||||
== "Error: tool3 is not a valid tool, try one of [tool1, tool2]."
|
||||
)
|
||||
assert tool_message.tool_call_id == "some 0"
|
||||
|
||||
# list of dicts tool content
|
||||
result3 = await ToolNode([tool3]).ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool3",
|
||||
"args": {"some_val": 2, "some_other_val": "bar"},
|
||||
"id": "some 0",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
tool_message: ToolMessage = result3["messages"][-1]
|
||||
assert tool_message.type == "tool"
|
||||
assert (
|
||||
tool_message.content
|
||||
== '[{"key_1": 2, "key_2": "foo"}, {"key_1": "bar", "key_2": "baz"}]'
|
||||
)
|
||||
assert tool_message.tool_call_id == "some 0"
|
||||
|
||||
# list of content blocks tool content
|
||||
result4 = await ToolNode([tool4]).ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool4",
|
||||
"args": {"some_val": 2, "some_other_val": "bar"},
|
||||
"id": "some 0",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
def test_tool_node_node_interrupt():
|
||||
def tool_normal(some_val: int) -> str:
|
||||
"""Tool docstring."""
|
||||
return "normal"
|
||||
|
||||
def tool_interrupt(some_val: int) -> str:
|
||||
"""Tool docstring."""
|
||||
raise NodeInterrupt("foo")
|
||||
|
||||
def handle(e: NodeInterrupt):
|
||||
return "handled"
|
||||
|
||||
for handle_tool_errors in (True, (NodeInterrupt,), "handled", handle, False):
|
||||
node = ToolNode([tool_interrupt], handle_tool_errors=handle_tool_errors)
|
||||
with pytest.raises(NodeInterrupt) as exc_info:
|
||||
node.invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool_interrupt",
|
||||
"args": {"some_val": 0},
|
||||
"id": "some 0",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
assert exc_info.value == "foo"
|
||||
|
||||
# test inside react agent
|
||||
model = FakeToolCallingModel(
|
||||
tool_calls=[
|
||||
[
|
||||
ToolCall(name="tool_interrupt", args={"some_val": 0}, id="1"),
|
||||
ToolCall(name="tool_normal", args={"some_val": 1}, id="2"),
|
||||
],
|
||||
[],
|
||||
]
|
||||
)
|
||||
tool_message: ToolMessage = result4["messages"][-1]
|
||||
assert tool_message.type == "tool"
|
||||
assert tool_message.content == [{"type": "image_url", "image_url": {"url": "abdc"}}]
|
||||
assert tool_message.tool_call_id == "some 0"
|
||||
checkpointer = MemorySaver()
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
agent = create_react_agent(
|
||||
model, [tool_interrupt, tool_normal], checkpointer=checkpointer
|
||||
)
|
||||
result = agent.invoke({"messages": [HumanMessage("hi?")]}, config)
|
||||
assert result["messages"] == [
|
||||
_AnyIdHumanMessage(
|
||||
content="hi?",
|
||||
),
|
||||
AIMessage(
|
||||
content="hi?",
|
||||
id="0",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool_interrupt",
|
||||
"args": {"some_val": 0},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
},
|
||||
{
|
||||
"name": "tool_normal",
|
||||
"args": {"some_val": 1},
|
||||
"id": "2",
|
||||
"type": "tool_call",
|
||||
},
|
||||
],
|
||||
),
|
||||
]
|
||||
state = agent.get_state(config)
|
||||
assert state.next == ("tools",)
|
||||
task = state.tasks[0]
|
||||
assert task.name == "tools"
|
||||
assert task.interrupts == (Interrupt(value="foo", when="during"),)
|
||||
|
||||
|
||||
def my_function(some_val: int, some_other_val: str) -> str:
|
||||
|
||||
@@ -17,7 +17,7 @@ from langgraph.pregel.types import StateSnapshot
|
||||
def test_with_config():
|
||||
# set up test
|
||||
remote_pregel = RemoteGraph(
|
||||
graph_id="test_graph_id",
|
||||
"test_graph_id",
|
||||
config={
|
||||
"configurable": {
|
||||
"foo": "bar",
|
||||
@@ -64,7 +64,7 @@ def test_get_graph():
|
||||
],
|
||||
}
|
||||
|
||||
remote_pregel = RemoteGraph(sync_client=mock_sync_client, graph_id="test_graph_id")
|
||||
remote_pregel = RemoteGraph("test_graph_id", sync_client=mock_sync_client)
|
||||
|
||||
# call method / assertions
|
||||
drawable_graph = remote_pregel.get_graph()
|
||||
@@ -111,7 +111,7 @@ async def test_aget_graph():
|
||||
],
|
||||
}
|
||||
|
||||
remote_pregel = RemoteGraph(client=mock_async_client, graph_id="test_graph_id")
|
||||
remote_pregel = RemoteGraph("test_graph_id", client=mock_async_client)
|
||||
|
||||
# call method / assertions
|
||||
drawable_graph = await remote_pregel.aget_graph()
|
||||
@@ -135,92 +135,6 @@ async def test_aget_graph():
|
||||
]
|
||||
|
||||
|
||||
def test_get_subgraphs():
|
||||
# set up test
|
||||
mock_sync_client = MagicMock()
|
||||
mock_sync_client.assistants.get_subgraphs.return_value = {
|
||||
"namespace_1": {
|
||||
"graph_id": "test_graph_id_2",
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"state_schema": {},
|
||||
"config_schema": {},
|
||||
},
|
||||
"namespace_2": {
|
||||
"graph_id": "test_graph_id_3",
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"state_schema": {},
|
||||
"config_schema": {},
|
||||
},
|
||||
}
|
||||
|
||||
remote_pregel = RemoteGraph(
|
||||
sync_client=mock_sync_client, graph_id="test_graph_id_1"
|
||||
)
|
||||
|
||||
# call method / assertions
|
||||
subgraphs = list(remote_pregel.get_subgraphs())
|
||||
assert len(subgraphs) == 2
|
||||
|
||||
subgraph_1 = subgraphs[0]
|
||||
ns_1 = subgraph_1[0]
|
||||
remote_pregel_1: RemoteGraph = subgraph_1[1]
|
||||
assert ns_1 == "namespace_1"
|
||||
assert remote_pregel_1.graph_id == "test_graph_id_2"
|
||||
|
||||
subgraph_2 = subgraphs[1]
|
||||
ns_2 = subgraph_2[0]
|
||||
remote_pregel_2: RemoteGraph = subgraph_2[1]
|
||||
assert ns_2 == "namespace_2"
|
||||
assert remote_pregel_2.graph_id == "test_graph_id_3"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_aget_subgraphs():
|
||||
# set up test
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.assistants.get_subgraphs.return_value = {
|
||||
"namespace_1": {
|
||||
"graph_id": "test_graph_id_2",
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"state_schema": {},
|
||||
"config_schema": {},
|
||||
},
|
||||
"namespace_2": {
|
||||
"graph_id": "test_graph_id_3",
|
||||
"input_schema": {},
|
||||
"output_schema": {},
|
||||
"state_schema": {},
|
||||
"config_schema": {},
|
||||
},
|
||||
}
|
||||
|
||||
remote_pregel = RemoteGraph(
|
||||
client=mock_async_client,
|
||||
graph_id="test_graph_id_1",
|
||||
)
|
||||
|
||||
# call method / assertions
|
||||
subgraphs = []
|
||||
async for subgraph in remote_pregel.aget_subgraphs():
|
||||
subgraphs.append(subgraph)
|
||||
assert len(subgraphs) == 2
|
||||
|
||||
subgraph_1 = subgraphs[0]
|
||||
ns_1 = subgraph_1[0]
|
||||
remote_pregel_1: RemoteGraph = subgraph_1[1]
|
||||
assert ns_1 == "namespace_1"
|
||||
assert remote_pregel_1.graph_id == "test_graph_id_2"
|
||||
|
||||
subgraph_2 = subgraphs[1]
|
||||
ns_2 = subgraph_2[0]
|
||||
remote_pregel_2: RemoteGraph = subgraph_2[1]
|
||||
assert ns_2 == "namespace_2"
|
||||
assert remote_pregel_2.graph_id == "test_graph_id_3"
|
||||
|
||||
|
||||
def test_get_state():
|
||||
# set up test
|
||||
mock_sync_client = MagicMock()
|
||||
@@ -240,7 +154,10 @@ def test_get_state():
|
||||
}
|
||||
|
||||
# call method / assertions
|
||||
remote_pregel = RemoteGraph(sync_client=mock_sync_client, graph_id="test_graph_id")
|
||||
remote_pregel = RemoteGraph(
|
||||
"test_graph_id",
|
||||
sync_client=mock_sync_client,
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "thread1"}}
|
||||
state_snapshot = remote_pregel.get_state(config)
|
||||
@@ -287,7 +204,10 @@ async def test_aget_state():
|
||||
}
|
||||
|
||||
# call method / assertions
|
||||
remote_pregel = RemoteGraph(client=mock_async_client, graph_id="test_graph_id")
|
||||
remote_pregel = RemoteGraph(
|
||||
"test_graph_id",
|
||||
client=mock_async_client,
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "thread1"}}
|
||||
state_snapshot = await remote_pregel.aget_state(config)
|
||||
@@ -338,7 +258,10 @@ def test_get_state_history():
|
||||
]
|
||||
|
||||
# call method / assertions
|
||||
remote_pregel = RemoteGraph(sync_client=mock_sync_client, graph_id="test_graph_id")
|
||||
remote_pregel = RemoteGraph(
|
||||
"test_graph_id",
|
||||
sync_client=mock_sync_client,
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "thread1"}}
|
||||
state_history_snapshot = list(
|
||||
@@ -386,7 +309,10 @@ async def test_aget_state_history():
|
||||
]
|
||||
|
||||
# call method / assertions
|
||||
remote_pregel = RemoteGraph(client=mock_async_client, graph_id="test_graph_id")
|
||||
remote_pregel = RemoteGraph(
|
||||
"test_graph_id",
|
||||
client=mock_async_client,
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "thread1"}}
|
||||
state_history_snapshot = []
|
||||
@@ -427,7 +353,10 @@ def test_update_state():
|
||||
}
|
||||
|
||||
# call method / assertions
|
||||
remote_pregel = RemoteGraph(sync_client=mock_sync_client, graph_id="test_graph_id")
|
||||
remote_pregel = RemoteGraph(
|
||||
"test_graph_id",
|
||||
sync_client=mock_sync_client,
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "thread1"}}
|
||||
response = remote_pregel.update_state(config, {"key": "value"})
|
||||
@@ -456,7 +385,10 @@ async def test_aupdate_state():
|
||||
}
|
||||
|
||||
# call method / assertions
|
||||
remote_pregel = RemoteGraph(client=mock_async_client, graph_id="test_graph_id")
|
||||
remote_pregel = RemoteGraph(
|
||||
"test_graph_id",
|
||||
client=mock_async_client,
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "thread1"}}
|
||||
response = await remote_pregel.aupdate_state(config, {"key": "value"})
|
||||
@@ -483,7 +415,10 @@ def test_stream():
|
||||
]
|
||||
|
||||
# call method / assertions
|
||||
remote_pregel = RemoteGraph(sync_client=mock_sync_client, graph_id="test_graph_id")
|
||||
remote_pregel = RemoteGraph(
|
||||
"test_graph_id",
|
||||
sync_client=mock_sync_client,
|
||||
)
|
||||
|
||||
# stream modes doesn't include 'updates'
|
||||
stream_parts = []
|
||||
@@ -583,7 +518,10 @@ async def test_astream():
|
||||
mock_async_client.runs.stream.return_value = async_iter
|
||||
|
||||
# call method / assertions
|
||||
remote_pregel = RemoteGraph(client=mock_async_client, graph_id="test_graph_id")
|
||||
remote_pregel = RemoteGraph(
|
||||
"test_graph_id",
|
||||
client=mock_async_client,
|
||||
)
|
||||
|
||||
# stream modes doesn't include 'updates'
|
||||
stream_parts = []
|
||||
@@ -717,7 +655,10 @@ def test_invoke():
|
||||
}
|
||||
|
||||
# call method / assertions
|
||||
remote_pregel = RemoteGraph(sync_client=mock_sync_client, graph_id="test_graph_id")
|
||||
remote_pregel = RemoteGraph(
|
||||
"test_graph_id",
|
||||
sync_client=mock_sync_client,
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "thread_1"}}
|
||||
result = remote_pregel.invoke(
|
||||
@@ -736,7 +677,10 @@ async def test_ainvoke():
|
||||
}
|
||||
|
||||
# call method / assertions
|
||||
remote_pregel = RemoteGraph(client=mock_async_client, graph_id="test_graph_id")
|
||||
remote_pregel = RemoteGraph(
|
||||
"test_graph_id",
|
||||
client=mock_async_client,
|
||||
)
|
||||
|
||||
config = {"configurable": {"thread_id": "thread_1"}}
|
||||
result = await remote_pregel.ainvoke(
|
||||
@@ -758,7 +702,9 @@ async def test_langgraph_cloud_integration():
|
||||
client = get_client()
|
||||
sync_client = get_sync_client()
|
||||
remote_pregel = RemoteGraph(
|
||||
client=client, sync_client=sync_client, graph_id="agent"
|
||||
"agent",
|
||||
client=client,
|
||||
sync_client=sync_client,
|
||||
)
|
||||
|
||||
# define graph
|
||||
@@ -836,9 +782,3 @@ async def test_langgraph_cloud_integration():
|
||||
remote_pregel.graph_id = "fe096781-5601-53d2-b2f6-0d3403f7e9ca" # must be UUID
|
||||
graph = await remote_pregel.aget_graph(xray=True)
|
||||
print("graph:", graph)
|
||||
|
||||
# test get subgraphs
|
||||
remote_pregel.graph_id = "fe096781-5601-53d2-b2f6-0d3403f7e9ca" # must be UUID
|
||||
async for name, pregel in remote_pregel.aget_subgraphs():
|
||||
print("name:", name)
|
||||
print("pregel:", pregel)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@langchain/langgraph-sdk",
|
||||
"version": "0.0.17",
|
||||
"version": "0.0.19",
|
||||
"description": "Client library for interacting with the LangGraph API",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
CronsCreatePayload,
|
||||
OnConflictBehavior,
|
||||
} from "./types.js";
|
||||
import { mergeSignals } from "./utils/signals.js";
|
||||
|
||||
interface ClientConfig {
|
||||
apiUrl?: string;
|
||||
@@ -56,6 +57,9 @@ class BaseClient {
|
||||
});
|
||||
|
||||
this.timeoutMs = config?.timeoutMs || 12_000;
|
||||
|
||||
// default limit being capped by Chrome
|
||||
// https://github.com/nodejs/undici/issues/1373
|
||||
this.apiUrl = config?.apiUrl || "http://localhost:8123";
|
||||
this.defaultHeaders = config?.defaultHeaders || {};
|
||||
if (config?.apiKey != null) {
|
||||
@@ -68,6 +72,7 @@ class BaseClient {
|
||||
options?: RequestInit & {
|
||||
json?: unknown;
|
||||
params?: Record<string, unknown>;
|
||||
timeoutMs?: number | null;
|
||||
},
|
||||
): [url: URL, init: RequestInit] {
|
||||
const mutatedOptions = {
|
||||
@@ -84,6 +89,16 @@ class BaseClient {
|
||||
delete mutatedOptions.json;
|
||||
}
|
||||
|
||||
let timeoutSignal: AbortSignal | null = null;
|
||||
if (typeof options?.timeoutMs !== "undefined") {
|
||||
if (options.timeoutMs != null) {
|
||||
timeoutSignal = AbortSignal.timeout(options.timeoutMs);
|
||||
}
|
||||
} else {
|
||||
timeoutSignal = AbortSignal.timeout(this.timeoutMs);
|
||||
}
|
||||
|
||||
mutatedOptions.signal = mergeSignals(timeoutSignal, mutatedOptions.signal);
|
||||
const targetUrl = new URL(`${this.apiUrl}${path}`);
|
||||
|
||||
if (mutatedOptions.params) {
|
||||
@@ -108,6 +123,8 @@ class BaseClient {
|
||||
options?: RequestInit & {
|
||||
json?: unknown;
|
||||
params?: Record<string, unknown>;
|
||||
timeoutMs?: number | null;
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
): Promise<T> {
|
||||
const response = await this.asyncCaller.fetch(
|
||||
@@ -689,6 +706,7 @@ export class RunsClient extends BaseClient {
|
||||
...this.prepareFetchOptions(endpoint, {
|
||||
method: "POST",
|
||||
json,
|
||||
timeoutMs: null,
|
||||
signal: payload?.signal,
|
||||
}),
|
||||
);
|
||||
@@ -834,11 +852,27 @@ export class RunsClient extends BaseClient {
|
||||
};
|
||||
const endpoint =
|
||||
threadId == null ? `/runs/wait` : `/threads/${threadId}/runs/wait`;
|
||||
return this.fetch<ThreadState["values"]>(endpoint, {
|
||||
const response = await this.fetch<ThreadState["values"]>(endpoint, {
|
||||
method: "POST",
|
||||
json,
|
||||
timeoutMs: null,
|
||||
signal: payload?.signal,
|
||||
});
|
||||
const raiseError =
|
||||
payload?.raiseError !== undefined ? payload.raiseError : true;
|
||||
if (
|
||||
raiseError &&
|
||||
"__error__" in response &&
|
||||
typeof response.__error__ === "object" &&
|
||||
response.__error__ &&
|
||||
"error" in response.__error__ &&
|
||||
"message" in response.__error__
|
||||
) {
|
||||
throw new Error(
|
||||
`${response.__error__?.error}: ${response.__error__?.message}`,
|
||||
);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -911,8 +945,15 @@ export class RunsClient extends BaseClient {
|
||||
* @param runId The ID of the run.
|
||||
* @returns
|
||||
*/
|
||||
async join(threadId: string, runId: string): Promise<void> {
|
||||
return this.fetch<void>(`/threads/${threadId}/runs/${runId}/join`);
|
||||
async join(
|
||||
threadId: string,
|
||||
runId: string,
|
||||
options?: { signal?: AbortSignal },
|
||||
): Promise<void> {
|
||||
return this.fetch<void>(`/threads/${threadId}/runs/${runId}/join`, {
|
||||
timeoutMs: null,
|
||||
signal: options?.signal,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -933,6 +974,7 @@ export class RunsClient extends BaseClient {
|
||||
const response = await this.asyncCaller.fetch(
|
||||
...this.prepareFetchOptions(`/threads/${threadId}/runs/${runId}/stream`, {
|
||||
method: "GET",
|
||||
timeoutMs: null,
|
||||
signal,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ type RunStatus =
|
||||
| "timeout"
|
||||
| "interrupted";
|
||||
|
||||
type ThreadStatus = "idle" | "busy" | "interrupted";
|
||||
type ThreadStatus = "idle" | "busy" | "interrupted" | "error";
|
||||
|
||||
type MultitaskStrategy = "reject" | "interrupt" | "rollback" | "enqueue";
|
||||
|
||||
|
||||
@@ -130,4 +130,9 @@ export interface CronsCreatePayload extends RunsCreatePayload {
|
||||
schedule: string;
|
||||
}
|
||||
|
||||
export type RunsWaitPayload = RunsStreamPayload;
|
||||
export interface RunsWaitPayload extends RunsStreamPayload {
|
||||
/**
|
||||
* Raise errors returned by the run. Default is `true`.
|
||||
*/
|
||||
raiseError?: boolean;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export function mergeSignals(...signals: (AbortSignal | null | undefined)[]) {
|
||||
const nonZeroSignals = signals.filter(
|
||||
(signal): signal is AbortSignal => signal != null,
|
||||
);
|
||||
|
||||
if (nonZeroSignals.length === 0) return undefined;
|
||||
if (nonZeroSignals.length === 1) return nonZeroSignals[0];
|
||||
|
||||
const controller = new AbortController();
|
||||
for (const signal of signals) {
|
||||
if (signal?.aborted) {
|
||||
controller.abort(signal.reason);
|
||||
return controller.signal;
|
||||
}
|
||||
|
||||
signal?.addEventListener("abort", () => controller.abort(signal.reason), {
|
||||
once: true,
|
||||
});
|
||||
}
|
||||
|
||||
return controller.signal;
|
||||
}
|
||||
@@ -159,7 +159,7 @@ def get_client(
|
||||
client = httpx.AsyncClient(
|
||||
base_url=url,
|
||||
transport=transport,
|
||||
timeout=httpx.Timeout(connect=5, read=60, write=60, pool=5),
|
||||
timeout=httpx.Timeout(connect=5, read=300, write=300, pool=5),
|
||||
headers=get_headers(api_key, headers),
|
||||
)
|
||||
return LangGraphClient(client)
|
||||
@@ -1202,6 +1202,7 @@ class RunsClient:
|
||||
feedback_keys: Optional[Sequence[str]] = None,
|
||||
on_disconnect: Optional[DisconnectMode] = None,
|
||||
on_completion: Optional[OnCompletionBehavior] = None,
|
||||
if_not_exists: Optional[IfNotExists] = None,
|
||||
webhook: Optional[str] = None,
|
||||
after_seconds: Optional[int] = None,
|
||||
) -> AsyncIterator[StreamPart]: ...
|
||||
@@ -1327,6 +1328,7 @@ class RunsClient:
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
||||
webhook: Optional[str] = None,
|
||||
on_completion: Optional[OnCompletionBehavior] = None,
|
||||
if_not_exists: Optional[IfNotExists] = None,
|
||||
after_seconds: Optional[int] = None,
|
||||
) -> Run: ...
|
||||
|
||||
@@ -1513,6 +1515,7 @@ class RunsClient:
|
||||
multitask_strategy: Optional[MultitaskStrategy] = None,
|
||||
if_not_exists: Optional[IfNotExists] = None,
|
||||
after_seconds: Optional[int] = None,
|
||||
raise_error: bool = True,
|
||||
) -> Union[list[dict], dict[str, Any]]: ...
|
||||
|
||||
@overload
|
||||
@@ -1529,7 +1532,9 @@ class RunsClient:
|
||||
webhook: Optional[str] = None,
|
||||
on_disconnect: Optional[DisconnectMode] = None,
|
||||
on_completion: Optional[OnCompletionBehavior] = None,
|
||||
if_not_exists: Optional[IfNotExists] = None,
|
||||
after_seconds: Optional[int] = None,
|
||||
raise_error: bool = True,
|
||||
) -> Union[list[dict], dict[str, Any]]: ...
|
||||
|
||||
async def wait(
|
||||
@@ -1550,6 +1555,7 @@ class RunsClient:
|
||||
multitask_strategy: Optional[MultitaskStrategy] = None,
|
||||
if_not_exists: Optional[IfNotExists] = None,
|
||||
after_seconds: Optional[int] = None,
|
||||
raise_error: bool = True,
|
||||
) -> Union[list[dict], dict[str, Any]]:
|
||||
"""Create a run, wait until it finishes and return the final state.
|
||||
|
||||
@@ -1642,9 +1648,19 @@ class RunsClient:
|
||||
endpoint = (
|
||||
f"/threads/{thread_id}/runs/wait" if thread_id is not None else "/runs/wait"
|
||||
)
|
||||
return await self.http.post(
|
||||
response = await self.http.post(
|
||||
endpoint, json={k: v for k, v in payload.items() if v is not None}
|
||||
)
|
||||
if (
|
||||
raise_error
|
||||
and isinstance(response, dict)
|
||||
and "__error__" in response
|
||||
and isinstance(response["__error__"], dict)
|
||||
):
|
||||
raise Exception(
|
||||
f"{response['__error__'].get('error')}: {response['__error__'].get('message')}"
|
||||
)
|
||||
return response
|
||||
|
||||
async def list(
|
||||
self, thread_id: str, *, limit: int = 10, offset: int = 0
|
||||
@@ -2257,7 +2273,7 @@ def get_sync_client(
|
||||
client = httpx.Client(
|
||||
base_url=url,
|
||||
transport=transport,
|
||||
timeout=httpx.Timeout(connect=5, read=60, write=60, pool=5),
|
||||
timeout=httpx.Timeout(connect=5, read=300, write=300, pool=5),
|
||||
headers=get_headers(api_key, headers),
|
||||
)
|
||||
return SyncLangGraphClient(client)
|
||||
@@ -3280,6 +3296,7 @@ class SyncRunsClient:
|
||||
feedback_keys: Optional[Sequence[str]] = None,
|
||||
on_disconnect: Optional[DisconnectMode] = None,
|
||||
on_completion: Optional[OnCompletionBehavior] = None,
|
||||
if_not_exists: Optional[IfNotExists] = None,
|
||||
webhook: Optional[str] = None,
|
||||
after_seconds: Optional[int] = None,
|
||||
) -> Iterator[StreamPart]: ...
|
||||
@@ -3405,6 +3422,7 @@ class SyncRunsClient:
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
||||
webhook: Optional[str] = None,
|
||||
on_completion: Optional[OnCompletionBehavior] = None,
|
||||
if_not_exists: Optional[IfNotExists] = None,
|
||||
after_seconds: Optional[int] = None,
|
||||
) -> Run: ...
|
||||
|
||||
@@ -3607,6 +3625,7 @@ class SyncRunsClient:
|
||||
webhook: Optional[str] = None,
|
||||
on_disconnect: Optional[DisconnectMode] = None,
|
||||
on_completion: Optional[OnCompletionBehavior] = None,
|
||||
if_not_exists: Optional[IfNotExists] = None,
|
||||
after_seconds: Optional[int] = None,
|
||||
) -> Union[list[dict], dict[str, Any]]: ...
|
||||
|
||||
|
||||
@@ -17,12 +17,13 @@ Represents the status of a run:
|
||||
- "interrupted": The run was manually stopped or interrupted.
|
||||
"""
|
||||
|
||||
ThreadStatus = Literal["idle", "busy", "interrupted"]
|
||||
ThreadStatus = Literal["idle", "busy", "interrupted", "error"]
|
||||
"""
|
||||
Represents the status of a thread:
|
||||
- "idle": The thread is not currently processing any task.
|
||||
- "busy": The thread is actively processing a task.
|
||||
- "interrupted": The thread's execution was interrupted.
|
||||
- "error": An exception occurred during task processing.
|
||||
"""
|
||||
|
||||
StreamMode = Literal["values", "messages", "updates", "events", "debug", "custom"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.33"
|
||||
version = "0.1.35"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user