mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-18 21:55:46 +02:00
Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6d6ab9b73 | ||
|
|
e7816eb197 | ||
|
|
4f8b69360c | ||
|
|
0718d0a660 | ||
|
|
3641e65cac | ||
|
|
b17141604d | ||
|
|
5fece1cbfc | ||
|
|
a85b7fca15 | ||
|
|
af9a09b182 | ||
|
|
75c8f99d3c | ||
|
|
46892855c5 | ||
|
|
59e8d59b7c | ||
|
|
5cb6ff79ca | ||
|
|
9c41ce0b1c | ||
|
|
61abee4bd4 | ||
|
|
43d9567c5d | ||
|
|
3923979479 | ||
|
|
5312fbd7f4 | ||
|
|
ae412bbede | ||
|
|
69c8d21ba6 | ||
|
|
2dfc4e3cc0 | ||
|
|
74c8589045 | ||
|
|
4717632ce7 | ||
|
|
726a85f26b | ||
|
|
8a0650a46b | ||
|
|
e7dc43b7ca | ||
|
|
90195af1d8 | ||
|
|
da707343dc | ||
|
|
1f5fc505c6 | ||
|
|
4c655f841e | ||
|
|
be27c96f3c | ||
|
|
5090e30f71 | ||
|
|
688de89864 | ||
|
|
62444d4c63 | ||
|
|
4a970cca8b | ||
|
|
18b9135770 | ||
|
|
d266ddb312 | ||
|
|
7a348ac19c | ||
|
|
123d93539a | ||
|
|
785e7dab3a | ||
|
|
582fb11dd4 | ||
|
|
ccfeafa975 |
@@ -0,0 +1,116 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import langgraph_cli
|
||||
import langgraph_cli.docker
|
||||
import langgraph_cli.config
|
||||
|
||||
from langgraph_cli.exec import Runner, subp_exec
|
||||
from langgraph_cli.progress import Progress
|
||||
from langgraph_cli.constants import DEFAULT_PORT
|
||||
|
||||
|
||||
def test(
|
||||
config: pathlib.Path,
|
||||
port: int,
|
||||
tag: str,
|
||||
verbose: bool,
|
||||
):
|
||||
with Runner() as runner, Progress(message="Pulling...") as set:
|
||||
# check docker available
|
||||
capabilities = langgraph_cli.docker.check_capabilities(runner)
|
||||
# open config
|
||||
with open(config) as f:
|
||||
config_json = langgraph_cli.config.validate_config(json.load(f))
|
||||
|
||||
set("Running...")
|
||||
args = [
|
||||
"run",
|
||||
"--rm",
|
||||
"-p",
|
||||
f"{port}:8000",
|
||||
]
|
||||
if isinstance(config_json["env"], str):
|
||||
args.extend(
|
||||
[
|
||||
"--env-file",
|
||||
str(config.parent / config_json["env"]),
|
||||
]
|
||||
)
|
||||
else:
|
||||
for k, v in config_json["env"].items():
|
||||
args.extend(
|
||||
[
|
||||
"-e",
|
||||
f"{k}={v}",
|
||||
]
|
||||
)
|
||||
if capabilities.healthcheck_start_interval:
|
||||
args.extend(
|
||||
[
|
||||
"--health-interval",
|
||||
"5s",
|
||||
"--health-retries",
|
||||
"1",
|
||||
"--health-start-period",
|
||||
"10s",
|
||||
"--health-start-interval",
|
||||
"1s",
|
||||
]
|
||||
)
|
||||
else:
|
||||
args.extend(
|
||||
[
|
||||
"--health-interval",
|
||||
"5s",
|
||||
"--health-retries",
|
||||
"2",
|
||||
]
|
||||
)
|
||||
|
||||
_task = None
|
||||
|
||||
def on_stdout(line: str):
|
||||
nonlocal _task
|
||||
if "GET /ok" in line or "Uvicorn running on" in line:
|
||||
set("")
|
||||
sys.stdout.write(
|
||||
f"""Ready!
|
||||
- API: http://localhost:{port}
|
||||
"""
|
||||
)
|
||||
sys.stdout.flush()
|
||||
_task.cancel()
|
||||
return True
|
||||
return False
|
||||
|
||||
async def subp_exec_task(*args, **kwargs):
|
||||
nonlocal _task
|
||||
_task = asyncio.create_task(subp_exec(*args, **kwargs))
|
||||
await _task
|
||||
|
||||
try:
|
||||
runner.run(
|
||||
subp_exec_task(
|
||||
"docker",
|
||||
*args,
|
||||
tag,
|
||||
verbose=verbose,
|
||||
on_stdout=on_stdout,
|
||||
)
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-t", "--tag", type=str)
|
||||
parser.add_argument("-c", "--config", type=str, default="./langgraph.json")
|
||||
parser.add_argument("-p", "--port", default=DEFAULT_PORT)
|
||||
args = parser.parse_args()
|
||||
test(pathlib.Path(args.config), args.port, args.tag, verbose=True)
|
||||
@@ -39,22 +39,36 @@ jobs:
|
||||
- name: Install cli globally
|
||||
if: steps.changed-files.outputs.all
|
||||
run: pip install -e .
|
||||
- name: Start service A
|
||||
- name: Build and test service A
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples
|
||||
run: |
|
||||
timeout 60 langgraph test -c examples/langgraph.json --verbose || (exit "$(($? == 124 ? 0 : $?))")
|
||||
- name: Start service B
|
||||
# The build-arg isn't used; just testing that we accept other args
|
||||
langgraph build -t langgraph-test-a --base-image "langchain/langgraph-trial"
|
||||
cp .env.example .envg
|
||||
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -c langgraph.json -t langgraph-test-a
|
||||
- name: Build and test service B
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples/graphs
|
||||
run: |
|
||||
timeout 60 langgraph test --verbose || (exit "$(($? == 124 ? 0 : $?))")
|
||||
- name: Start service C
|
||||
langgraph build -t langgraph-test-b --base-image "langchain/langgraph-trial"
|
||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-b
|
||||
- name: Build and test service C
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples/graphs_reqs_a
|
||||
run: |
|
||||
timeout 60 langgraph test --verbose || (exit "$(($? == 124 ? 0 : $?))")
|
||||
- name: Start service D
|
||||
langgraph build -t langgraph-test-c --base-image "langchain/langgraph-trial"
|
||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-c
|
||||
- name: Build and test service D
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/examples/graphs_reqs_b
|
||||
run: |
|
||||
timeout 60 langgraph test --verbose || (exit "$(($? == 124 ? 0 : $?))")
|
||||
langgraph build -t langgraph-test-d --base-image "langchain/langgraph-trial"
|
||||
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-d
|
||||
|
||||
- name: Build JS service
|
||||
if: steps.changed-files.outputs.all
|
||||
working-directory: libs/cli/js-examples
|
||||
run: |
|
||||
langgraph build -t langgraph-test-e
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- "latest"
|
||||
include:
|
||||
- python-version: "3.11"
|
||||
core-version: ">=0.2.39,<0.3.0"
|
||||
core-version: ">=0.2.42,<0.3.0"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
|
||||
@@ -93,3 +93,5 @@ jobs:
|
||||
# This is *only for CI use* and is *extremely dangerous* otherwise!
|
||||
# https://github.com/pypa/gh-action-pypi-publish#tolerating-release-package-file-duplicates
|
||||
skip-existing: true
|
||||
# Temp workaround since attestations are on by default as of gh-action-pypi-publish v1.11.0
|
||||
attestations: false
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -270,6 +270,8 @@ jobs:
|
||||
packages-dir: ${{ inputs.working-directory }}/dist/
|
||||
verbose: true
|
||||
print-hash: true
|
||||
# Temp workaround since attestations are on by default as of gh-action-pypi-publish v1.11.0
|
||||
attestations: false
|
||||
|
||||
mark-release:
|
||||
needs:
|
||||
|
||||
@@ -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.
|
||||
|
||||
+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.
|
||||
@@ -0,0 +1,6 @@
|
||||
# RemoteGraph
|
||||
|
||||
::: langgraph.pregel.remote
|
||||
options:
|
||||
members:
|
||||
- RemoteGraph
|
||||
@@ -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.
|
||||
|
||||
+14
-122
@@ -26,6 +26,7 @@ theme:
|
||||
- navigation.instant.progress
|
||||
- navigation.prune
|
||||
- navigation.tabs
|
||||
- navigation.tabs.sticky
|
||||
- navigation.top
|
||||
- navigation.tracking
|
||||
- search.highlight
|
||||
@@ -79,129 +80,13 @@ 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
|
||||
- RemoteGraph: reference/remote_graph.md
|
||||
- Checkpointing: reference/checkpoints.md
|
||||
- Storage: reference/store.md
|
||||
- Prebuilt Components: reference/prebuilt.md
|
||||
@@ -351,7 +236,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
@@ -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]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
@@ -0,0 +1,10 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,json,yml}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -0,0 +1,3 @@
|
||||
# Copy this over:
|
||||
# cp .env.example .env
|
||||
# Then modify to suit your needs
|
||||
@@ -0,0 +1,62 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"prettier",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 12,
|
||||
parser: "@typescript-eslint/parser",
|
||||
project: "./tsconfig.json",
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["import", "@typescript-eslint", "no-instanceof"],
|
||||
ignorePatterns: [
|
||||
".eslintrc.cjs",
|
||||
"scripts",
|
||||
"src/utils/lodash/*",
|
||||
"node_modules",
|
||||
"dist",
|
||||
"dist-cjs",
|
||||
"*.js",
|
||||
"*.cjs",
|
||||
"*.d.ts",
|
||||
],
|
||||
rules: {
|
||||
"no-process-env": 2,
|
||||
"no-instanceof/no-instanceof": 2,
|
||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||
"@typescript-eslint/no-empty-function": 0,
|
||||
"@typescript-eslint/no-shadow": 0,
|
||||
"@typescript-eslint/no-empty-interface": 0,
|
||||
"@typescript-eslint/no-use-before-define": ["error", "nofunc"],
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-misused-promises": "error",
|
||||
camelcase: 0,
|
||||
"class-methods-use-this": 0,
|
||||
"import/extensions": [2, "ignorePackages"],
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{ devDependencies: ["**/*.test.ts"] },
|
||||
],
|
||||
"import/no-unresolved": 0,
|
||||
"import/prefer-default-export": 0,
|
||||
"keyword-spacing": "error",
|
||||
"max-classes-per-file": 0,
|
||||
"max-len": 0,
|
||||
"no-await-in-loop": 0,
|
||||
"no-bitwise": 0,
|
||||
"no-console": 0,
|
||||
"no-restricted-syntax": 0,
|
||||
"no-shadow": 0,
|
||||
"no-continue": 0,
|
||||
"no-underscore-dangle": 0,
|
||||
"no-use-before-define": 0,
|
||||
"no-useless-constructor": 0,
|
||||
"no-return-await": 0,
|
||||
"consistent-return": 0,
|
||||
"no-else-return": 0,
|
||||
"new-cap": ["error", { properties: false, capIsNew: false }],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
index.cjs
|
||||
index.js
|
||||
index.d.ts
|
||||
node_modules
|
||||
dist
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
.turbo
|
||||
**/.turbo
|
||||
**/.eslintcache
|
||||
|
||||
.env
|
||||
.ipynb_checkpoints
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 LangChain
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,79 @@
|
||||
# New LangGraph.js Project
|
||||
|
||||
[](https://github.com/langchain-ai/new-langgraphjs-project/actions/workflows/unit-tests.yml)
|
||||
[](https://github.com/langchain-ai/new-langgraphjs-project/actions/workflows/integration-tests.yml)
|
||||
[](https://langgraph-studio.vercel.app/templates/open?githubUrl=https://github.com/langchain-ai/new-langgraphjs-project)
|
||||
|
||||
This template demonstrates a simple chatbot implemented using [LangGraph.js](https://github.com/langchain-ai/langgraphjs), designed for [LangGraph Studio](https://github.com/langchain-ai/langgraph-studio). The chatbot maintains persistent chat memory, allowing for coherent conversations across multiple interactions.
|
||||
|
||||

|
||||
|
||||
The core logic, defined in `src/agent/graph.ts`, showcases a straightforward chatbot that responds to user queries while maintaining context from previous messages.
|
||||
|
||||
## What it does
|
||||
|
||||
The simple chatbot:
|
||||
|
||||
1. Takes a user **message** as input
|
||||
2. Maintains a history of the conversation
|
||||
3. Returns a placeholder response, updating the conversation history
|
||||
|
||||
This template provides a foundation that can be easily customized and extended to create more complex conversational agents.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Assuming you have already [installed LangGraph Studio](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file#download), to set up:
|
||||
|
||||
1. Create a `.env` file. This template does not require any environment variables by default, but you will likely want to add some when customizing.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
<!--
|
||||
Setup instruction auto-generated by `langgraph template lock`. DO NOT EDIT MANUALLY.
|
||||
-->
|
||||
|
||||
<!--
|
||||
End setup instructions
|
||||
-->
|
||||
|
||||
2. Open the folder in LangGraph Studio!
|
||||
3. Customize the code as needed.
|
||||
|
||||
## How to customize
|
||||
|
||||
1. **Add an LLM call**: You can select and install a chat model wrapper from [the LangChain.js ecosystem](https://js.langchain.com/docs/integrations/chat/), or use LangGraph.js without LangChain.js.
|
||||
2. **Extend the graph**: The core logic of the chatbot is defined in [graph.ts](./src/agent/graph.ts). You can modify this file to add new nodes, edges, or change the flow of the conversation.
|
||||
|
||||
You can also extend this template by:
|
||||
|
||||
- Adding [custom tools or functions](https://js.langchain.com/docs/how_to/tool_calling) to enhance the chatbot's capabilities.
|
||||
- Implementing additional logic for handling specific types of user queries or tasks.
|
||||
- Add retrieval-augmented generation (RAG) capabilities by integrating [external APIs or databases](https://langchain-ai.github.io/langgraphjs/tutorials/rag/langgraph_agentic_rag/) to provide more customized responses.
|
||||
|
||||
## Development
|
||||
|
||||
While iterating on your graph, you can edit past state and rerun your app from previous states to debug specific nodes. Local changes will be automatically applied via hot reload. Try experimenting with:
|
||||
|
||||
- Modifying the system prompt to give your chatbot a unique personality.
|
||||
- Adding new nodes to the graph for more complex conversation flows.
|
||||
- Implementing conditional logic to handle different types of user inputs.
|
||||
|
||||
Follow-up requests will be appended to the same thread. You can create an entirely new thread, clearing previous history, using the `+` button in the top right.
|
||||
|
||||
For more advanced features and examples, refer to the [LangGraph.js documentation](https://github.com/langchain-ai/langgraphjs). These resources can help you adapt this template for your specific use case and build more sophisticated conversational agents.
|
||||
|
||||
LangGraph Studio also integrates with [LangSmith](https://smith.langchain.com/) for more in-depth tracing and collaboration with teammates, allowing you to analyze and optimize your chatbot's performance.
|
||||
|
||||
<!--
|
||||
Configuration auto-generated by `langgraph template lock`. DO NOT EDIT MANUALLY.
|
||||
{
|
||||
"config_schemas": {
|
||||
"agent": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
-->
|
||||
@@ -0,0 +1,18 @@
|
||||
export default {
|
||||
preset: "ts-jest/presets/default-esm",
|
||||
moduleNameMapper: {
|
||||
"^(\\.{1,2}/.*)\\.js$": "$1",
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.tsx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
extensionsToTreatAsEsm: [".ts"],
|
||||
setupFiles: ["dotenv/config"],
|
||||
passWithNoTests: true,
|
||||
testTimeout: 20_000,
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"node_version": "20",
|
||||
"graphs": {
|
||||
"agent": "./src/agent/graph.ts:graph"
|
||||
},
|
||||
"env": ".env",
|
||||
"dependencies": ["."]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "example-graph",
|
||||
"version": "0.0.1",
|
||||
"description": "A starter template for creating a LangGraph workflow.",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"main": "my_app/graph.ts",
|
||||
"author": "Your Name",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.test\\.ts$ --testPathIgnorePatterns=\\.int\\.test\\.ts$",
|
||||
"test:int": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.int\\.test\\.ts$",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint src",
|
||||
"format:check": "prettier --check .",
|
||||
"lint:langgraph-json": "node scripts/checkLanggraphPaths.js",
|
||||
"lint:all": "yarn lint & yarn lint:langgraph-json & yarn format:check",
|
||||
"test:all": "yarn test && yarn test:int && yarn lint:langgraph"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/core": "^0.3.2",
|
||||
"@langchain/langgraph": "^0.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@tsconfig/recommended": "^1.0.7",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||
"@typescript-eslint/parser": "^5.59.8",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-no-instanceof": "^1.0.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.3.3",
|
||||
"ts-jest": "^29.1.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Starter LangGraph.js Template
|
||||
* Make this code your own!
|
||||
*/
|
||||
import { StateGraph } from "@langchain/langgraph";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { StateAnnotation } from "./state.js";
|
||||
|
||||
/**
|
||||
* Define a node, these do the work of the graph and should have most of the logic.
|
||||
* Must return a subset of the properties set in StateAnnotation.
|
||||
* @param state The current state of the graph.
|
||||
* @param config Extra parameters passed into the state graph.
|
||||
* @returns Some subset of parameters of the graph state, used to update the state
|
||||
* for the edges and nodes executed next.
|
||||
*/
|
||||
const callModel = async (
|
||||
state: typeof StateAnnotation.State,
|
||||
_config: RunnableConfig,
|
||||
): Promise<typeof StateAnnotation.Update> => {
|
||||
/**
|
||||
* Do some work... (e.g. call an LLM)
|
||||
* For example, with LangChain you could do something like:
|
||||
*
|
||||
* ```bash
|
||||
* $ npm i @langchain/anthropic
|
||||
* ```
|
||||
*
|
||||
* ```ts
|
||||
* import { ChatAnthropic } from "@langchain/anthropic";
|
||||
* const model = new ChatAnthropic({
|
||||
* model: "claude-3-5-sonnet-20240620",
|
||||
* apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
* });
|
||||
* const res = await model.invoke(state.messages);
|
||||
* ```
|
||||
*
|
||||
* Or, with an SDK directly:
|
||||
*
|
||||
* ```bash
|
||||
* $ npm i openai
|
||||
* ```
|
||||
*
|
||||
* ```ts
|
||||
* import OpenAI from "openai";
|
||||
* const openai = new OpenAI({
|
||||
* apiKey: process.env.OPENAI_API_KEY,
|
||||
* });
|
||||
*
|
||||
* const chatCompletion = await openai.chat.completions.create({
|
||||
* messages: [{
|
||||
* role: state.messages[0]._getType(),
|
||||
* content: state.messages[0].content,
|
||||
* }],
|
||||
* model: "gpt-4o-mini",
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
console.log("Current state:", state);
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: `Hi there! How are you?`,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Routing function: Determines whether to continue research or end the builder.
|
||||
* This function decides if the gathered information is satisfactory or if more research is needed.
|
||||
*
|
||||
* @param state - The current state of the research builder
|
||||
* @returns Either "callModel" to continue research or END to finish the builder
|
||||
*/
|
||||
export const route = (
|
||||
state: typeof StateAnnotation.State,
|
||||
): "__end__" | "callModel" => {
|
||||
if (state.messages.length > 0) {
|
||||
return "__end__";
|
||||
}
|
||||
// Loop back
|
||||
return "callModel";
|
||||
};
|
||||
|
||||
// Finally, create the graph itself.
|
||||
const builder = new StateGraph(StateAnnotation)
|
||||
// Add the nodes to do the work.
|
||||
// Chaining the nodes together in this way
|
||||
// updates the types of the StateGraph instance
|
||||
// so you have static type checking when it comes time
|
||||
// to add the edges.
|
||||
.addNode("callModel", callModel)
|
||||
// Regular edges mean "always transition to node B after node A is done"
|
||||
// The "__start__" and "__end__" nodes are "virtual" nodes that are always present
|
||||
// and represent the beginning and end of the builder.
|
||||
.addEdge("__start__", "callModel")
|
||||
// Conditional edges optionally route to different nodes (or end)
|
||||
.addConditionalEdges("callModel", route);
|
||||
|
||||
export const graph = builder.compile();
|
||||
|
||||
graph.name = "New Agent";
|
||||
@@ -0,0 +1,59 @@
|
||||
import { BaseMessage, BaseMessageLike } from "@langchain/core/messages";
|
||||
import { Annotation, messagesStateReducer } from "@langchain/langgraph";
|
||||
|
||||
/**
|
||||
* A graph's StateAnnotation defines three main things:
|
||||
* 1. The structure of the data to be passed between nodes (which "channels" to read from/write to and their types)
|
||||
* 2. Default values for each field
|
||||
* 3. Reducers for the state's. Reducers are functions that determine how to apply updates to the state.
|
||||
* See [Reducers](https://langchain-ai.github.io/langgraphjs/concepts/low_level/#reducers) for more information.
|
||||
*/
|
||||
|
||||
// This is the primary state of your agent, where you can store any information
|
||||
export const StateAnnotation = Annotation.Root({
|
||||
/**
|
||||
* Messages track the primary execution state of the agent.
|
||||
*
|
||||
* Typically accumulates a pattern of:
|
||||
*
|
||||
* 1. HumanMessage - user input
|
||||
* 2. AIMessage with .tool_calls - agent picking tool(s) to use to collect
|
||||
* information
|
||||
* 3. ToolMessage(s) - the responses (or errors) from the executed tools
|
||||
*
|
||||
* (... repeat steps 2 and 3 as needed ...)
|
||||
* 4. AIMessage without .tool_calls - agent responding in unstructured
|
||||
* format to the user.
|
||||
*
|
||||
* 5. HumanMessage - user responds with the next conversational turn.
|
||||
*
|
||||
* (... repeat steps 2-5 as needed ... )
|
||||
*
|
||||
* Merges two lists of messages or message-like objects with role and content,
|
||||
* updating existing messages by ID.
|
||||
*
|
||||
* Message-like objects are automatically coerced by `messagesStateReducer` into
|
||||
* LangChain message classes. If a message does not have a given id,
|
||||
* LangGraph will automatically assign one.
|
||||
*
|
||||
* By default, this ensures the state is "append-only", unless the
|
||||
* new message has the same ID as an existing message.
|
||||
*
|
||||
* Returns:
|
||||
* A new list of messages with the messages from \`right\` merged into \`left\`.
|
||||
* If a message in \`right\` has the same ID as a message in \`left\`, the
|
||||
* message from \`right\` will replace the message from \`left\`.`
|
||||
*/
|
||||
messages: Annotation<BaseMessage[], BaseMessageLike[]>({
|
||||
reducer: messagesStateReducer,
|
||||
default: () => [],
|
||||
}),
|
||||
/**
|
||||
* Feel free to add additional attributes to your state as needed.
|
||||
* Common examples include retrieved documents, extracted entities, API connections, etc.
|
||||
*
|
||||
* For simple fields whose value should be overwritten by the return value of a node,
|
||||
* you don't need to define a reducer or default.
|
||||
*/
|
||||
// additionalField: Annotation<string>,
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 583 KiB |
@@ -0,0 +1,8 @@
|
||||
import { describe, it, expect } from "@jest/globals";
|
||||
import { route } from "../src/agent/graph.js";
|
||||
describe("Routers", () => {
|
||||
it("Test route", async () => {
|
||||
const res = route({ messages: [] });
|
||||
expect(res).toEqual("callModel");
|
||||
}, 100_000);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, it, expect } from "@jest/globals";
|
||||
import { graph } from "../src/agent/graph.js";
|
||||
|
||||
describe("Graph", () => {
|
||||
it("should process input through the graph", async () => {
|
||||
const input = "What is the capital of France?";
|
||||
const result = await graph.invoke({ input });
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe("object");
|
||||
expect(result.messages).toBeDefined();
|
||||
expect(Array.isArray(result.messages)).toBe(true);
|
||||
expect(result.messages.length).toBeGreaterThan(0);
|
||||
|
||||
const lastMessage = result.messages[result.messages.length - 1];
|
||||
expect(lastMessage.content.toString().toLowerCase()).toContain("hi");
|
||||
}, 30000); // Increased timeout to 30 seconds
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "@tsconfig/recommended",
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"lib": ["ES2021", "ES2022.Object", "DOM"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "nodenext",
|
||||
"esModuleInterop": true,
|
||||
"noImplicitReturns": true,
|
||||
"declaration": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"useDefineForClassFields": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"strictFunctionTypes": false,
|
||||
"outDir": "dist",
|
||||
"types": ["jest", "node"],
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.js"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+23
-124
@@ -1,7 +1,8 @@
|
||||
import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import sys
|
||||
from typing import Callable, Optional
|
||||
from typing import Callable, Optional, Sequence
|
||||
|
||||
import click
|
||||
import click.exceptions
|
||||
@@ -260,117 +261,15 @@ For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KE
|
||||
)
|
||||
|
||||
|
||||
@OPT_PULL
|
||||
@OPT_PORT
|
||||
@OPT_CONFIG
|
||||
@OPT_VERBOSE
|
||||
@cli.command(
|
||||
help="Start langgraph test server. This command enables you to confirm your graph will work inside the langgraph API server, before using LangGraph Cloud."
|
||||
)
|
||||
@log_command
|
||||
def test(
|
||||
config: pathlib.Path,
|
||||
port: int,
|
||||
pull: bool,
|
||||
# stop_when_ready: bool,
|
||||
verbose: bool,
|
||||
):
|
||||
with Runner() as runner, Progress(message="Pulling...") as set:
|
||||
# check docker available
|
||||
capabilities = langgraph_cli.docker.check_capabilities(runner)
|
||||
# open config
|
||||
with open(config) as f:
|
||||
config_json = langgraph_cli.config.validate_config(json.load(f))
|
||||
# build
|
||||
base_image = "langchain/langgraph-trial"
|
||||
tag = f"langgraph-test-{config.parent.name}"
|
||||
_build(
|
||||
runner,
|
||||
set,
|
||||
config,
|
||||
config_json,
|
||||
None,
|
||||
base_image,
|
||||
pull,
|
||||
tag,
|
||||
)
|
||||
# run
|
||||
set("Running...")
|
||||
args = [
|
||||
"run",
|
||||
"--rm",
|
||||
"-p",
|
||||
f"{port}:8000",
|
||||
]
|
||||
if isinstance(config_json["env"], str):
|
||||
args.extend(
|
||||
[
|
||||
"--env-file",
|
||||
str(config.parent / config_json["env"]),
|
||||
]
|
||||
)
|
||||
else:
|
||||
for k, v in config_json["env"].items():
|
||||
args.extend(
|
||||
[
|
||||
"-e",
|
||||
f"{k}={v}",
|
||||
]
|
||||
)
|
||||
if capabilities.healthcheck_start_interval:
|
||||
args.extend(
|
||||
[
|
||||
"--health-interval",
|
||||
"5s",
|
||||
"--health-retries",
|
||||
"1",
|
||||
"--health-start-period",
|
||||
"10s",
|
||||
"--health-start-interval",
|
||||
"1s",
|
||||
]
|
||||
)
|
||||
else:
|
||||
args.extend(
|
||||
[
|
||||
"--health-interval",
|
||||
"5s",
|
||||
"--health-retries",
|
||||
"2",
|
||||
]
|
||||
)
|
||||
|
||||
def on_stdout(line: str):
|
||||
if "GET /ok" in line:
|
||||
set("")
|
||||
sys.stdout.write(
|
||||
f"""Ready!
|
||||
- API: http://localhost:{port}
|
||||
"""
|
||||
)
|
||||
sys.stdout.flush()
|
||||
return True
|
||||
|
||||
runner.run(
|
||||
subp_exec(
|
||||
"docker",
|
||||
*args,
|
||||
tag,
|
||||
verbose=verbose,
|
||||
on_stdout=on_stdout,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _build(
|
||||
runner,
|
||||
set: Callable[[str], None],
|
||||
config: pathlib.Path,
|
||||
config_json: dict,
|
||||
platform: Optional[str],
|
||||
base_image: Optional[str],
|
||||
pull: bool,
|
||||
tag: str,
|
||||
passthrough: Sequence[str] = (),
|
||||
):
|
||||
base_image = base_image or (
|
||||
"langchain/langgraphjs-api"
|
||||
@@ -398,14 +297,18 @@ def _build(
|
||||
"-t",
|
||||
tag,
|
||||
]
|
||||
if platform:
|
||||
args.extend(["--platform", platform])
|
||||
# apply config
|
||||
stdin = langgraph_cli.config.config_to_docker(config, config_json, base_image)
|
||||
# run docker build
|
||||
runner.run(
|
||||
subp_exec(
|
||||
"docker", "build", *args, str(config.parent), input=stdin, verbose=True
|
||||
"docker",
|
||||
"build",
|
||||
*args,
|
||||
*passthrough,
|
||||
str(config.parent),
|
||||
input=stdin,
|
||||
verbose=True,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -425,37 +328,33 @@ def _build(
|
||||
""",
|
||||
required=True,
|
||||
)
|
||||
@click.option(
|
||||
"--platform",
|
||||
help="""Target platform(s) to build the docker image for.
|
||||
|
||||
\b
|
||||
Example:
|
||||
langgraph build --platform linux/amd64,linux/arm64
|
||||
\b
|
||||
""",
|
||||
)
|
||||
@click.option(
|
||||
"--base-image",
|
||||
hidden=True,
|
||||
)
|
||||
@cli.command(help="Build langgraph API server docker image")
|
||||
@click.argument("docker_build_args", nargs=-1, type=click.UNPROCESSED)
|
||||
@cli.command(
|
||||
help="Build langgraph API server docker image",
|
||||
context_settings=dict(
|
||||
ignore_unknown_options=True,
|
||||
),
|
||||
)
|
||||
@log_command
|
||||
def build(
|
||||
config: pathlib.Path,
|
||||
platform: Optional[str],
|
||||
docker_build_args: Sequence[str],
|
||||
base_image: Optional[str],
|
||||
pull: bool,
|
||||
tag: str,
|
||||
):
|
||||
with Runner() as runner, Progress(message="Pulling...") as set:
|
||||
# check docker available
|
||||
langgraph_cli.docker.check_capabilities(runner)
|
||||
# open config
|
||||
if shutil.which("docker") is None:
|
||||
raise click.UsageError("Docker not installed") from None
|
||||
with open(config) as f:
|
||||
config_json = langgraph_cli.config.validate_config(json.load(f))
|
||||
# build
|
||||
_build(runner, set, config, config_json, platform, base_image, pull, tag)
|
||||
_build(
|
||||
runner, set, config, config_json, base_image, pull, tag, docker_build_args
|
||||
)
|
||||
|
||||
|
||||
@OPT_CONFIG
|
||||
|
||||
@@ -65,6 +65,8 @@ CONFIG_KEY_CHECKPOINT_ID = sys.intern("checkpoint_id")
|
||||
# holds the current checkpoint_id, if any
|
||||
CONFIG_KEY_CHECKPOINT_NS = sys.intern("checkpoint_ns")
|
||||
# holds the current checkpoint_ns, "" for root graph
|
||||
CONFIG_KEY_NODE_FINISHED = sys.intern("__pregel_node_finished")
|
||||
# callback to be called when a node is finished
|
||||
|
||||
# --- Other constants ---
|
||||
PUSH = sys.intern("__pregel_push")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Callable, Sequence, Union, cast
|
||||
from typing import Any, Callable, Sequence, Union
|
||||
|
||||
from langchain_core.load.serializable import Serializable
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
@@ -101,8 +101,7 @@ class ToolExecutor(RunnableCallable):
|
||||
) -> None:
|
||||
super().__init__(self._execute, afunc=self._aexecute, trace=False)
|
||||
tools_ = [
|
||||
tool if isinstance(tool, BaseTool) else cast(BaseTool, create_tool(tool))
|
||||
for tool in tools
|
||||
tool if isinstance(tool, BaseTool) else create_tool(tool) for tool in tools
|
||||
]
|
||||
self.tools = tools_
|
||||
self.tool_map = {t.name: t for t in tools_}
|
||||
|
||||
@@ -34,6 +34,7 @@ from langchain_core.runnables.config import (
|
||||
from langchain_core.runnables.utils import Input
|
||||
from langchain_core.tools import BaseTool, InjectedToolArg
|
||||
from langchain_core.tools import tool as create_tool
|
||||
from langchain_core.tools.base import get_all_basemodel_annotations
|
||||
from typing_extensions import Annotated, get_args, get_origin
|
||||
|
||||
from langgraph.errors import GraphInterrupt
|
||||
@@ -200,7 +201,7 @@ class ToolNode(RunnableCallable):
|
||||
self.messages_key = messages_key
|
||||
for tool_ in tools:
|
||||
if not isinstance(tool_, BaseTool):
|
||||
tool_ = cast(BaseTool, create_tool(tool_))
|
||||
tool_ = create_tool(tool_)
|
||||
self.tools_by_name[tool_.name] = tool_
|
||||
self.tool_to_state_args[tool_.name] = _get_state_args(tool_)
|
||||
self.tool_to_store_arg[tool_.name] = _get_store_arg(tool_)
|
||||
@@ -659,7 +660,7 @@ def _get_state_args(tool: BaseTool) -> Dict[str, Optional[str]]:
|
||||
full_schema = tool.get_input_schema()
|
||||
tool_args_to_state_fields: Dict = {}
|
||||
|
||||
for name, type_ in full_schema.__annotations__.items():
|
||||
for name, type_ in get_all_basemodel_annotations(full_schema).items():
|
||||
injections = [
|
||||
type_arg
|
||||
for type_arg in get_args(type_)
|
||||
@@ -683,7 +684,7 @@ def _get_state_args(tool: BaseTool) -> Dict[str, Optional[str]]:
|
||||
|
||||
def _get_store_arg(tool: BaseTool) -> Optional[str]:
|
||||
full_schema = tool.get_input_schema()
|
||||
for name, type_ in full_schema.__annotations__.items():
|
||||
for name, type_ in get_all_basemodel_annotations(full_schema).items():
|
||||
injections = [
|
||||
type_arg
|
||||
for type_arg in get_args(type_)
|
||||
|
||||
@@ -56,6 +56,7 @@ from langgraph.constants import (
|
||||
CONF,
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
CONFIG_KEY_CHECKPOINTER,
|
||||
CONFIG_KEY_NODE_FINISHED,
|
||||
CONFIG_KEY_READ,
|
||||
CONFIG_KEY_RESUMING,
|
||||
CONFIG_KEY_SEND,
|
||||
@@ -1288,6 +1289,7 @@ class Pregel(PregelProtocol):
|
||||
runner = PregelRunner(
|
||||
submit=loop.submit,
|
||||
put_writes=loop.put_writes,
|
||||
node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
|
||||
)
|
||||
# enable subgraph streaming
|
||||
if subgraphs:
|
||||
@@ -1509,6 +1511,7 @@ class Pregel(PregelProtocol):
|
||||
submit=loop.submit,
|
||||
put_writes=loop.put_writes,
|
||||
use_astream=do_stream is not None,
|
||||
node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
|
||||
)
|
||||
# enable subgraph streaming
|
||||
if subgraphs:
|
||||
|
||||
@@ -116,7 +116,7 @@ def DuplexStream(*streams: StreamProtocol) -> StreamProtocol:
|
||||
def __call__(value: StreamChunk) -> None:
|
||||
for stream in streams:
|
||||
if value[1] in stream.modes:
|
||||
stream(value) # type: ignore
|
||||
stream(value)
|
||||
|
||||
return StreamProtocol(__call__, {mode for s in streams for mode in s.modes})
|
||||
|
||||
@@ -587,7 +587,7 @@ class PregelLoop(LoopProtocol):
|
||||
if mode not in self.stream.modes:
|
||||
return
|
||||
for v in values(*args, **kwargs):
|
||||
self.stream((self.checkpoint_ns, mode, v)) # type: ignore
|
||||
self.stream((self.checkpoint_ns, mode, v))
|
||||
|
||||
def _output_writes(
|
||||
self, task_id: str, writes: Sequence[tuple[str, Any]], *, cached: bool = False
|
||||
|
||||
@@ -31,11 +31,17 @@ from langgraph_sdk.schema import StreamMode as StreamModeSDK
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.checkpoint.base import CheckpointMetadata
|
||||
from langgraph.constants import INTERRUPT
|
||||
from langgraph.constants import (
|
||||
CONF,
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
CONFIG_KEY_STREAM,
|
||||
INTERRUPT,
|
||||
NS_SEP,
|
||||
)
|
||||
from langgraph.errors import GraphInterrupt
|
||||
from langgraph.pregel.protocol import PregelProtocol
|
||||
from langgraph.pregel.types import All, PregelTask, StateSnapshot, StreamMode
|
||||
from langgraph.types import Interrupt
|
||||
from langgraph.types import Interrupt, StreamProtocol
|
||||
from langgraph.utils.config import merge_configs
|
||||
|
||||
|
||||
@@ -46,6 +52,16 @@ class RemoteException(Exception):
|
||||
|
||||
|
||||
class RemoteGraph(PregelProtocol):
|
||||
"""The `RemoteGraph` class is a client implementation for calling remote
|
||||
APIs that implement the LangGraph Server API specification.
|
||||
|
||||
For example, the `RemoteGraph` class can be used to call APIs from deployments
|
||||
on LangGraph Cloud.
|
||||
|
||||
`RemoteGraph` behaves the same way as a `Graph` and can be used directly as
|
||||
a node in another `Graph`.
|
||||
"""
|
||||
|
||||
name: str
|
||||
|
||||
def __init__(
|
||||
@@ -63,14 +79,42 @@ class RemoteGraph(PregelProtocol):
|
||||
"""Specify `url`, `api_key`, and/or `headers` to create default sync and async clients.
|
||||
|
||||
If `client` or `sync_client` are provided, they will be used instead of the default clients.
|
||||
See `LangGraphClient` and `SyncLangGraphClient` for details on the default clients.
|
||||
See `LangGraphClient` and `SyncLangGraphClient` for details on the default clients. At least
|
||||
one of `url`, `client`, or `sync_client` must be provided.
|
||||
|
||||
Args:
|
||||
name: The name of the graph.
|
||||
url: The URL of the remote API.
|
||||
api_key: The API key to use for authentication. If not provided, it will be read from the environment (`LANGGRAPH_API_KEY`, `LANGSMITH_API_KEY`, or `LANGCHAIN_API_KEY`).
|
||||
headers: Additional headers to include in the requests.
|
||||
client: A `LangGraphClient` instance to use instead of creating a default client.
|
||||
sync_client: A `SyncLangGraphClient` instance to use instead of creating a default client.
|
||||
config: An optional `RunnableConfig` instance with additional configuration.
|
||||
"""
|
||||
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(
|
||||
url=url, api_key=api_key, headers=headers
|
||||
)
|
||||
|
||||
if client is None and url is not None:
|
||||
client = get_client(url=url, api_key=api_key, headers=headers)
|
||||
self.client = client
|
||||
|
||||
if sync_client is None and url is not None:
|
||||
sync_client = get_sync_client(url=url, api_key=api_key, headers=headers)
|
||||
self.sync_client = sync_client
|
||||
|
||||
def _validate_client(self) -> LangGraphClient:
|
||||
if self.client is None:
|
||||
raise ValueError(
|
||||
"Async client is not initialized: please provide `url` or `client` when initializing `RemoteGraph`."
|
||||
)
|
||||
return self.client
|
||||
|
||||
def _validate_sync_client(self) -> SyncLangGraphClient:
|
||||
if self.sync_client is None:
|
||||
raise ValueError(
|
||||
"Sync client is not initialized: please provide `url` or `sync_client` when initializing `RemoteGraph`."
|
||||
)
|
||||
return self.sync_client
|
||||
|
||||
def copy(self, update: dict[str, Any]) -> Self:
|
||||
attrs = {**self.__dict__, **update}
|
||||
@@ -103,7 +147,21 @@ class RemoteGraph(PregelProtocol):
|
||||
*,
|
||||
xray: Union[int, bool] = False,
|
||||
) -> DrawableGraph:
|
||||
graph = self.sync_client.assistants.get_graph(
|
||||
"""Get graph by graph name.
|
||||
|
||||
This method calls `GET /assistants/{assistant_id}/graph`.
|
||||
|
||||
Args:
|
||||
config: This parameter is not used.
|
||||
xray: Include graph representation of subgraphs. If an integer
|
||||
value is provided, only subgraphs with a depth less than or
|
||||
equal to the value will be included.
|
||||
|
||||
Returns:
|
||||
The graph information for the assistant in JSON format.
|
||||
"""
|
||||
sync_client = self._validate_sync_client()
|
||||
graph = sync_client.assistants.get_graph(
|
||||
assistant_id=self.name,
|
||||
xray=xray,
|
||||
)
|
||||
@@ -118,7 +176,21 @@ class RemoteGraph(PregelProtocol):
|
||||
*,
|
||||
xray: Union[int, bool] = False,
|
||||
) -> DrawableGraph:
|
||||
graph = await self.client.assistants.get_graph(
|
||||
"""Get graph by graph name.
|
||||
|
||||
This method calls `GET /assistants/{assistant_id}/graph`.
|
||||
|
||||
Args:
|
||||
config: This parameter is not used.
|
||||
xray: Include graph representation of subgraphs. If an integer
|
||||
value is provided, only subgraphs with a depth less than or
|
||||
equal to the value will be included.
|
||||
|
||||
Returns:
|
||||
The graph information for the assistant in JSON format.
|
||||
"""
|
||||
client = self._validate_client()
|
||||
graph = await client.assistants.get_graph(
|
||||
assistant_id=self.name,
|
||||
xray=xray,
|
||||
)
|
||||
@@ -143,7 +215,7 @@ class RemoteGraph(PregelProtocol):
|
||||
interrupts=tuple(interrupts),
|
||||
state=self._create_state_snapshot(task["state"])
|
||||
if task["state"]
|
||||
else {"configurable": task["checkpoint"]}
|
||||
else cast(RunnableConfig, {"configurable": task["checkpoint"]})
|
||||
if task["checkpoint"]
|
||||
else None,
|
||||
result=task.get("result"),
|
||||
@@ -248,9 +320,24 @@ class RemoteGraph(PregelProtocol):
|
||||
def get_state(
|
||||
self, config: RunnableConfig, *, subgraphs: bool = False
|
||||
) -> StateSnapshot:
|
||||
"""Get the state of a thread.
|
||||
|
||||
This method calls `POST /threads/{thread_id}/state/checkpoint` if a
|
||||
checkpoint is specified in the config or `GET /threads/{thread_id}/state`
|
||||
if no checkpoint is specified.
|
||||
|
||||
Args:
|
||||
config: A `RunnableConfig` that includes `thread_id` in the
|
||||
`configurable` field.
|
||||
subgraphs: Include subgraphs in the state.
|
||||
|
||||
Returns:
|
||||
The latest state of the thread.
|
||||
"""
|
||||
sync_client = self._validate_sync_client()
|
||||
merged_config = merge_configs(self.config, config)
|
||||
|
||||
state = self.sync_client.threads.get_state(
|
||||
state = sync_client.threads.get_state(
|
||||
thread_id=merged_config["configurable"]["thread_id"],
|
||||
checkpoint=self._get_checkpoint(merged_config),
|
||||
subgraphs=subgraphs,
|
||||
@@ -260,9 +347,24 @@ class RemoteGraph(PregelProtocol):
|
||||
async def aget_state(
|
||||
self, config: RunnableConfig, *, subgraphs: bool = False
|
||||
) -> StateSnapshot:
|
||||
"""Get the state of a thread.
|
||||
|
||||
This method calls `POST /threads/{thread_id}/state/checkpoint` if a
|
||||
checkpoint is specified in the config or `GET /threads/{thread_id}/state`
|
||||
if no checkpoint is specified.
|
||||
|
||||
Args:
|
||||
config: A `RunnableConfig` that includes `thread_id` in the
|
||||
`configurable` field.
|
||||
subgraphs: Include subgraphs in the state.
|
||||
|
||||
Returns:
|
||||
The latest state of the thread.
|
||||
"""
|
||||
client = self._validate_client()
|
||||
merged_config = merge_configs(self.config, config)
|
||||
|
||||
state = await self.client.threads.get_state(
|
||||
state = await client.threads.get_state(
|
||||
thread_id=merged_config["configurable"]["thread_id"],
|
||||
checkpoint=self._get_checkpoint(merged_config),
|
||||
subgraphs=subgraphs,
|
||||
@@ -277,9 +379,24 @@ class RemoteGraph(PregelProtocol):
|
||||
before: Optional[RunnableConfig] = None,
|
||||
limit: Optional[int] = None,
|
||||
) -> Iterator[StateSnapshot]:
|
||||
"""Get the state history of a thread.
|
||||
|
||||
This method calls `POST /threads/{thread_id}/history`.
|
||||
|
||||
Args:
|
||||
config: A `RunnableConfig` that includes `thread_id` in the
|
||||
`configurable` field.
|
||||
filter: Metadata to filter on.
|
||||
before: A `RunnableConfig` that includes checkpoint metadata.
|
||||
limit: Max number of states to return.
|
||||
|
||||
Returns:
|
||||
States of the thread.
|
||||
"""
|
||||
sync_client = self._validate_sync_client()
|
||||
merged_config = merge_configs(self.config, config)
|
||||
|
||||
states = self.sync_client.threads.get_history(
|
||||
states = sync_client.threads.get_history(
|
||||
thread_id=merged_config["configurable"]["thread_id"],
|
||||
limit=limit if limit else 10,
|
||||
before=self._get_checkpoint(before),
|
||||
@@ -297,9 +414,24 @@ class RemoteGraph(PregelProtocol):
|
||||
before: Optional[RunnableConfig] = None,
|
||||
limit: Optional[int] = None,
|
||||
) -> AsyncIterator[StateSnapshot]:
|
||||
"""Get the state history of a thread.
|
||||
|
||||
This method calls `POST /threads/{thread_id}/history`.
|
||||
|
||||
Args:
|
||||
config: A `RunnableConfig` that includes `thread_id` in the
|
||||
`configurable` field.
|
||||
filter: Metadata to filter on.
|
||||
before: A `RunnableConfig` that includes checkpoint metadata.
|
||||
limit: Max number of states to return.
|
||||
|
||||
Returns:
|
||||
States of the thread.
|
||||
"""
|
||||
client = self._validate_client()
|
||||
merged_config = merge_configs(self.config, config)
|
||||
|
||||
states = await self.client.threads.get_history(
|
||||
states = await client.threads.get_history(
|
||||
thread_id=merged_config["configurable"]["thread_id"],
|
||||
limit=limit if limit else 10,
|
||||
before=self._get_checkpoint(before),
|
||||
@@ -315,9 +447,23 @@ class RemoteGraph(PregelProtocol):
|
||||
values: Optional[Union[dict[str, Any], Any]],
|
||||
as_node: Optional[str] = None,
|
||||
) -> RunnableConfig:
|
||||
"""Update the state of a thread.
|
||||
|
||||
This method calls `POST /threads/{thread_id}/state`.
|
||||
|
||||
Args:
|
||||
config: A `RunnableConfig` that includes `thread_id` in the
|
||||
`configurable` field.
|
||||
values: Values to update to the state.
|
||||
as_node: Update the state as if this node had just executed.
|
||||
|
||||
Returns:
|
||||
`RunnableConfig` for the updated thread.
|
||||
"""
|
||||
sync_client = self._validate_sync_client()
|
||||
merged_config = merge_configs(self.config, config)
|
||||
|
||||
response: dict = self.sync_client.threads.update_state( # type: ignore
|
||||
response: dict = sync_client.threads.update_state( # type: ignore
|
||||
thread_id=merged_config["configurable"]["thread_id"],
|
||||
values=values,
|
||||
as_node=as_node,
|
||||
@@ -331,9 +477,23 @@ class RemoteGraph(PregelProtocol):
|
||||
values: Optional[Union[dict[str, Any], Any]],
|
||||
as_node: Optional[str] = None,
|
||||
) -> RunnableConfig:
|
||||
"""Update the state of a thread.
|
||||
|
||||
This method calls `POST /threads/{thread_id}/state`.
|
||||
|
||||
Args:
|
||||
config: A `RunnableConfig` that includes `thread_id` in the
|
||||
`configurable` field.
|
||||
values: Values to update to the state.
|
||||
as_node: Update the state as if this node had just executed.
|
||||
|
||||
Returns:
|
||||
`RunnableConfig` for the updated thread.
|
||||
"""
|
||||
client = self._validate_client()
|
||||
merged_config = merge_configs(self.config, config)
|
||||
|
||||
response: dict = await self.client.threads.update_state( # type: ignore
|
||||
response: dict = await client.threads.update_state( # type: ignore
|
||||
thread_id=merged_config["configurable"]["thread_id"],
|
||||
values=values,
|
||||
as_node=as_node,
|
||||
@@ -353,7 +513,7 @@ class RemoteGraph(PregelProtocol):
|
||||
'updates' mode is added to the list of stream modes so that interrupts
|
||||
can be detected in the remote graph.
|
||||
"""
|
||||
updated_stream_modes: list[StreamMode] = []
|
||||
updated_stream_modes: list[StreamModeSDK] = []
|
||||
req_updates = False
|
||||
req_single = True
|
||||
# coerce to list, or add default stream mode
|
||||
@@ -365,6 +525,10 @@ class RemoteGraph(PregelProtocol):
|
||||
updated_stream_modes.extend(stream_mode)
|
||||
else:
|
||||
updated_stream_modes.append(default)
|
||||
# map "messages" to "messages-tuple"
|
||||
if "messages" in updated_stream_modes:
|
||||
updated_stream_modes.remove("messages")
|
||||
updated_stream_modes.append("messages-tuple")
|
||||
# add 'updates' mode if not present
|
||||
if "updates" in updated_stream_modes:
|
||||
req_updates = True
|
||||
@@ -382,21 +546,55 @@ class RemoteGraph(PregelProtocol):
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
||||
subgraphs: bool = False,
|
||||
) -> Iterator[Union[dict[str, Any], Any]]:
|
||||
"""Create a run and stream the results.
|
||||
|
||||
This method calls `POST /threads/{thread_id}/runs/stream` if a `thread_id`
|
||||
is speciffed in the `configurable` field of the config or
|
||||
`POST /runs/stream` otherwise.
|
||||
|
||||
Args:
|
||||
input: Input to the graph.
|
||||
config: A `RunnableConfig` for graph invocation.
|
||||
stream_mode: Stream mode(s) to use.
|
||||
interrupt_before: Interrupt the graph before these nodes.
|
||||
interrupt_after: Interrupt the graph after these nodes.
|
||||
subgraphs: Stream from subgraphs.
|
||||
|
||||
Yields:
|
||||
The output of the graph.
|
||||
"""
|
||||
sync_client = self._validate_sync_client()
|
||||
merged_config = merge_configs(self.config, config)
|
||||
sanitized_config = self._sanitize_config(merged_config)
|
||||
stream_modes, req_updates, req_single = self._get_stream_modes(stream_mode)
|
||||
stream: Optional[StreamProtocol] = (
|
||||
(config or {}).get(CONF, {}).get(CONFIG_KEY_STREAM)
|
||||
)
|
||||
stream_modes_ext: list[StreamModeSDK] = (
|
||||
[*stream_modes, *stream.modes] if stream else stream_modes
|
||||
)
|
||||
|
||||
for chunk in self.sync_client.runs.stream(
|
||||
for chunk in sync_client.runs.stream(
|
||||
thread_id=sanitized_config["configurable"].get("thread_id"),
|
||||
assistant_id=self.name,
|
||||
input=input,
|
||||
config=sanitized_config,
|
||||
stream_mode=stream_modes,
|
||||
stream_mode=stream_modes_ext,
|
||||
interrupt_before=interrupt_before,
|
||||
interrupt_after=interrupt_after,
|
||||
stream_subgraphs=subgraphs,
|
||||
stream_subgraphs=subgraphs or stream is not None,
|
||||
if_not_exists="create",
|
||||
):
|
||||
if NS_SEP in chunk.event:
|
||||
mode, ns_ = chunk.event.split(NS_SEP, 1)
|
||||
ns = tuple(ns_.split(NS_SEP))
|
||||
else:
|
||||
mode, ns = chunk.event, ()
|
||||
if caller_ns := (config or {}).get(CONF, {}).get(CONFIG_KEY_CHECKPOINT_NS):
|
||||
caller_ns = tuple(caller_ns.split(NS_SEP))
|
||||
ns = caller_ns + ns
|
||||
if stream is not None and chunk.event in stream.modes:
|
||||
stream((ns, mode, chunk.data))
|
||||
if chunk.event.startswith("updates"):
|
||||
if isinstance(chunk.data, dict) and INTERRUPT in chunk.data:
|
||||
raise GraphInterrupt(chunk.data[INTERRUPT])
|
||||
@@ -404,10 +602,12 @@ class RemoteGraph(PregelProtocol):
|
||||
continue
|
||||
elif chunk.event.startswith("error"):
|
||||
raise RemoteException(chunk.data)
|
||||
if chunk.event.split(NS_SEP, 1)[0] not in stream_modes:
|
||||
continue
|
||||
if subgraphs:
|
||||
if "|" in chunk.event:
|
||||
mode, ns_ = chunk.event.split("|", 1)
|
||||
ns = tuple(ns_.split("|"))
|
||||
if NS_SEP in chunk.event:
|
||||
mode, ns_ = chunk.event.split(NS_SEP, 1)
|
||||
ns = tuple(ns_.split(NS_SEP))
|
||||
else:
|
||||
mode, ns = chunk.event, ()
|
||||
if req_single:
|
||||
@@ -429,21 +629,55 @@ class RemoteGraph(PregelProtocol):
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
||||
subgraphs: bool = False,
|
||||
) -> AsyncIterator[Union[dict[str, Any], Any]]:
|
||||
"""Create a run and stream the results.
|
||||
|
||||
This method calls `POST /threads/{thread_id}/runs/stream` if a `thread_id`
|
||||
is speciffed in the `configurable` field of the config or
|
||||
`POST /runs/stream` otherwise.
|
||||
|
||||
Args:
|
||||
input: Input to the graph.
|
||||
config: A `RunnableConfig` for graph invocation.
|
||||
stream_mode: Stream mode(s) to use.
|
||||
interrupt_before: Interrupt the graph before these nodes.
|
||||
interrupt_after: Interrupt the graph after these nodes.
|
||||
subgraphs: Stream from subgraphs.
|
||||
|
||||
Yields:
|
||||
The output of the graph.
|
||||
"""
|
||||
client = self._validate_client()
|
||||
merged_config = merge_configs(self.config, config)
|
||||
sanitized_config = self._sanitize_config(merged_config)
|
||||
stream_modes, req_updates, req_single = self._get_stream_modes(stream_mode)
|
||||
stream: Optional[StreamProtocol] = (
|
||||
(config or {}).get(CONF, {}).get(CONFIG_KEY_STREAM)
|
||||
)
|
||||
stream_modes_ext: list[StreamModeSDK] = (
|
||||
[*stream_modes, *stream.modes] if stream else stream_modes
|
||||
)
|
||||
|
||||
async for chunk in self.client.runs.stream(
|
||||
async for chunk in client.runs.stream(
|
||||
thread_id=sanitized_config["configurable"].get("thread_id"),
|
||||
assistant_id=self.name,
|
||||
input=input,
|
||||
config=sanitized_config,
|
||||
stream_mode=stream_modes,
|
||||
stream_mode=stream_modes_ext,
|
||||
interrupt_before=interrupt_before,
|
||||
interrupt_after=interrupt_after,
|
||||
stream_subgraphs=subgraphs,
|
||||
stream_subgraphs=subgraphs or stream is not None,
|
||||
if_not_exists="create",
|
||||
):
|
||||
if NS_SEP in chunk.event:
|
||||
mode, ns_ = chunk.event.split(NS_SEP, 1)
|
||||
ns = tuple(ns_.split(NS_SEP))
|
||||
else:
|
||||
mode, ns = chunk.event, ()
|
||||
if caller_ns := (config or {}).get(CONF, {}).get(CONFIG_KEY_CHECKPOINT_NS):
|
||||
caller_ns = tuple(caller_ns.split(NS_SEP))
|
||||
ns = caller_ns + ns
|
||||
if stream is not None and chunk.event in stream.modes:
|
||||
stream((ns, mode, chunk.data))
|
||||
if chunk.event.startswith("updates"):
|
||||
if isinstance(chunk.data, dict) and INTERRUPT in chunk.data:
|
||||
raise GraphInterrupt(chunk.data[INTERRUPT])
|
||||
@@ -451,12 +685,9 @@ class RemoteGraph(PregelProtocol):
|
||||
continue
|
||||
elif chunk.event.startswith("error"):
|
||||
raise RemoteException(chunk.data)
|
||||
if chunk.event.split(NS_SEP, 1)[0] not in stream_modes:
|
||||
continue
|
||||
if subgraphs:
|
||||
if "|" in chunk.event:
|
||||
mode, ns_ = chunk.event.split("|", 1)
|
||||
ns = tuple(ns_.split("|"))
|
||||
else:
|
||||
mode, ns = chunk.event, ()
|
||||
if req_single:
|
||||
yield ns, chunk.data
|
||||
else:
|
||||
@@ -490,18 +721,33 @@ class RemoteGraph(PregelProtocol):
|
||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
||||
) -> Union[dict[str, Any], Any]:
|
||||
merged_config = merge_configs(self.config, config)
|
||||
sanitized_config = self._sanitize_config(merged_config)
|
||||
"""Create a run, wait until it finishes and return the final state.
|
||||
|
||||
return self.sync_client.runs.wait(
|
||||
thread_id=sanitized_config["configurable"].get("thread_id"),
|
||||
assistant_id=self.name,
|
||||
input=input,
|
||||
config=sanitized_config,
|
||||
This method calls `POST /threads/{thread_id}/runs/wait` if a `thread_id`
|
||||
is speciffed in the `configurable` field of the config or
|
||||
`POST /runs/wait` otherwise.
|
||||
|
||||
Args:
|
||||
input: Input to the graph.
|
||||
config: A `RunnableConfig` for graph invocation.
|
||||
interrupt_before: Interrupt the graph before these nodes.
|
||||
interrupt_after: Interrupt the graph after these nodes.
|
||||
|
||||
Returns:
|
||||
The output of the graph.
|
||||
"""
|
||||
for chunk in self.stream(
|
||||
input,
|
||||
config=config,
|
||||
interrupt_before=interrupt_before,
|
||||
interrupt_after=interrupt_after,
|
||||
if_not_exists="create",
|
||||
)
|
||||
stream_mode="values",
|
||||
):
|
||||
pass
|
||||
try:
|
||||
return chunk
|
||||
except UnboundLocalError:
|
||||
return None
|
||||
|
||||
async def ainvoke(
|
||||
self,
|
||||
@@ -511,15 +757,30 @@ class RemoteGraph(PregelProtocol):
|
||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
||||
) -> Union[dict[str, Any], Any]:
|
||||
merged_config = merge_configs(self.config, config)
|
||||
sanitized_config = self._sanitize_config(merged_config)
|
||||
"""Create a run, wait until it finishes and return the final state.
|
||||
|
||||
return await self.client.runs.wait(
|
||||
thread_id=sanitized_config["configurable"].get("thread_id"),
|
||||
assistant_id=self.name,
|
||||
input=input,
|
||||
config=sanitized_config,
|
||||
This method calls `POST /threads/{thread_id}/runs/wait` if a `thread_id`
|
||||
is speciffed in the `configurable` field of the config or
|
||||
`POST /runs/wait` otherwise.
|
||||
|
||||
Args:
|
||||
input: Input to the graph.
|
||||
config: A `RunnableConfig` for graph invocation.
|
||||
interrupt_before: Interrupt the graph before these nodes.
|
||||
interrupt_after: Interrupt the graph after these nodes.
|
||||
|
||||
Returns:
|
||||
The output of the graph.
|
||||
"""
|
||||
async for chunk in self.astream(
|
||||
input,
|
||||
config=config,
|
||||
interrupt_before=interrupt_before,
|
||||
interrupt_after=interrupt_after,
|
||||
if_not_exists="create",
|
||||
)
|
||||
stream_mode="values",
|
||||
):
|
||||
pass
|
||||
try:
|
||||
return chunk
|
||||
except UnboundLocalError:
|
||||
return None
|
||||
|
||||
@@ -14,7 +14,7 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
from langgraph.constants import ERROR, INTERRUPT, NO_WRITES
|
||||
from langgraph.constants import ERROR, INTERRUPT, NO_WRITES, TAG_HIDDEN
|
||||
from langgraph.errors import GraphDelegate, GraphInterrupt
|
||||
from langgraph.pregel.executor import Submit
|
||||
from langgraph.pregel.retry import arun_with_retry, run_with_retry
|
||||
@@ -32,10 +32,12 @@ class PregelRunner:
|
||||
submit: Submit,
|
||||
put_writes: Callable[[str, Sequence[tuple[str, Any]]], None],
|
||||
use_astream: bool = False,
|
||||
node_finished: Optional[Callable[[str], None]] = None,
|
||||
) -> None:
|
||||
self.submit = submit
|
||||
self.put_writes = put_writes
|
||||
self.use_astream = use_astream
|
||||
self.node_finished = node_finished
|
||||
|
||||
def tick(
|
||||
self,
|
||||
@@ -209,6 +211,10 @@ class PregelRunner:
|
||||
# save error to checkpointer
|
||||
self.put_writes(task.id, [(ERROR, exception)])
|
||||
else:
|
||||
if self.node_finished and (
|
||||
task.config is None or TAG_HIDDEN not in task.config.get("tags", [])
|
||||
):
|
||||
self.node_finished(task.name)
|
||||
if not task.writes:
|
||||
# add no writes marker
|
||||
task.writes.append((NO_WRITES, None))
|
||||
|
||||
@@ -10,9 +10,11 @@ from typing import (
|
||||
Sequence,
|
||||
Type,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointMetadata
|
||||
|
||||
@@ -227,14 +229,14 @@ class StreamProtocol:
|
||||
|
||||
modes: set[StreamMode]
|
||||
|
||||
__call__: Callable[[StreamChunk], None]
|
||||
__call__: Callable[[Self, StreamChunk], None]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
__call__: Callable[[StreamChunk], None],
|
||||
modes: set[StreamMode],
|
||||
) -> None:
|
||||
self.__call__ = __call__
|
||||
self.__call__ = cast(Callable[[Self, StreamChunk], None], __call__)
|
||||
self.modes = modes
|
||||
|
||||
|
||||
|
||||
Generated
+893
-828
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.2.39"
|
||||
version = "0.2.41"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -9,7 +9,7 @@ repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9.0,<4.0"
|
||||
langchain-core = ">=0.2.39,<0.4"
|
||||
langchain-core = ">=0.2.42,<0.4.0,!=0.3.0,!=0.3.1,!=0.3.2,!=0.3.3,!=0.3.4,!=0.3.5,!=0.3.6,!=0.3.7,!=0.3.8,!=0.3.9,!=0.3.10,!=0.3.11,!=0.3.12,!=0.3.13"
|
||||
langgraph-checkpoint = "^2.0.0"
|
||||
langgraph-sdk = "^0.1.32"
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -50,6 +50,7 @@ from langgraph.prebuilt.tool_node import (
|
||||
TOOL_CALL_ERROR_TEMPLATE,
|
||||
InjectedState,
|
||||
InjectedStore,
|
||||
_get_state_args,
|
||||
_infer_handled_types,
|
||||
)
|
||||
from langgraph.store.base import BaseStore
|
||||
@@ -1332,3 +1333,18 @@ async def test_return_direct() -> None:
|
||||
id=result["messages"][3].id,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test__get_state_args() -> None:
|
||||
class Schema1(BaseModel):
|
||||
a: Annotated[str, InjectedState]
|
||||
|
||||
class Schema2(Schema1):
|
||||
b: Annotated[int, InjectedState("bar")]
|
||||
|
||||
@dec_tool(args_schema=Schema2)
|
||||
def foo(a: str, b: int) -> float:
|
||||
"""return"""
|
||||
return 0.0
|
||||
|
||||
assert _get_state_args(foo) == {"a": None, "b": "bar"}
|
||||
|
||||
@@ -54,7 +54,7 @@ from langgraph.checkpoint.base import (
|
||||
CheckpointTuple,
|
||||
)
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.constants import ERROR, PULL, PUSH
|
||||
from langgraph.constants import CONFIG_KEY_NODE_FINISHED, ERROR, PULL, PUSH
|
||||
from langgraph.errors import InvalidUpdateError, MultipleSubgraphsError, NodeInterrupt
|
||||
from langgraph.graph import END, Graph
|
||||
from langgraph.graph.graph import START
|
||||
@@ -8917,15 +8917,27 @@ def test_doubly_nested_graph_interrupts(
|
||||
}
|
||||
|
||||
# test stream updates w/ nested interrupt
|
||||
config = {"configurable": {"thread_id": "2"}}
|
||||
nodes: list[str] = []
|
||||
config = {
|
||||
"configurable": {"thread_id": "2", CONFIG_KEY_NODE_FINISHED: nodes.append}
|
||||
}
|
||||
assert [*app.stream({"my_key": "my value"}, config)] == [
|
||||
{"parent_1": {"my_key": "hi my value"}},
|
||||
{"__interrupt__": ()},
|
||||
]
|
||||
assert nodes == ["parent_1", "grandchild_1"]
|
||||
assert [*app.stream(None, config)] == [
|
||||
{"child": {"my_key": "hi my value here and there"}},
|
||||
{"parent_2": {"my_key": "hi my value here and there and back again"}},
|
||||
]
|
||||
assert nodes == [
|
||||
"parent_1",
|
||||
"grandchild_1",
|
||||
"grandchild_2",
|
||||
"child_1",
|
||||
"child",
|
||||
"parent_2",
|
||||
]
|
||||
|
||||
# test stream values w/ nested interrupt
|
||||
config = {"configurable": {"thread_id": "3"}}
|
||||
|
||||
@@ -50,7 +50,7 @@ from langgraph.checkpoint.base import (
|
||||
CheckpointTuple,
|
||||
)
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.constants import ERROR, PULL, PUSH
|
||||
from langgraph.constants import CONFIG_KEY_NODE_FINISHED, ERROR, PULL, PUSH
|
||||
from langgraph.errors import InvalidUpdateError, MultipleSubgraphsError, NodeInterrupt
|
||||
from langgraph.graph import END, Graph, StateGraph
|
||||
from langgraph.graph.graph import START
|
||||
@@ -7605,15 +7605,27 @@ async def test_doubly_nested_graph_interrupts(checkpointer_name: str) -> None:
|
||||
}
|
||||
|
||||
# test stream updates w/ nested interrupt
|
||||
config = {"configurable": {"thread_id": "2"}}
|
||||
nodes: list[str] = []
|
||||
config = {
|
||||
"configurable": {"thread_id": "2", CONFIG_KEY_NODE_FINISHED: nodes.append}
|
||||
}
|
||||
assert [c async for c in app.astream({"my_key": "my value"}, config)] == [
|
||||
{"parent_1": {"my_key": "hi my value"}},
|
||||
{"__interrupt__": ()},
|
||||
]
|
||||
assert nodes == ["parent_1", "grandchild_1"]
|
||||
assert [c async for c in app.astream(None, config)] == [
|
||||
{"child": {"my_key": "hi my value here and there"}},
|
||||
{"parent_2": {"my_key": "hi my value here and there and back again"}},
|
||||
]
|
||||
assert nodes == [
|
||||
"parent_1",
|
||||
"grandchild_1",
|
||||
"grandchild_2",
|
||||
"child_1",
|
||||
"child",
|
||||
"parent_2",
|
||||
]
|
||||
|
||||
# test stream values w/ nested interrupt
|
||||
config = {"configurable": {"thread_id": "3"}}
|
||||
|
||||
@@ -650,9 +650,13 @@ async def test_astream():
|
||||
def test_invoke():
|
||||
# set up test
|
||||
mock_sync_client = MagicMock()
|
||||
mock_sync_client.runs.wait.return_value = {
|
||||
"values": {"messages": [{"type": "human", "content": "world"}]}
|
||||
}
|
||||
mock_sync_client.runs.stream.return_value = [
|
||||
StreamPart(event="values", data={"chunk": "data1"}),
|
||||
StreamPart(event="values", data={"chunk": "data2"}),
|
||||
StreamPart(
|
||||
event="values", data={"messages": [{"type": "human", "content": "world"}]}
|
||||
),
|
||||
]
|
||||
|
||||
# call method / assertions
|
||||
remote_pregel = RemoteGraph(
|
||||
@@ -665,16 +669,22 @@ def test_invoke():
|
||||
{"input": {"messages": [{"type": "human", "content": "hello"}]}}, config
|
||||
)
|
||||
|
||||
assert result == {"values": {"messages": [{"type": "human", "content": "world"}]}}
|
||||
assert result == {"messages": [{"type": "human", "content": "world"}]}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_ainvoke():
|
||||
# set up test
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.runs.wait.return_value = {
|
||||
"values": {"messages": [{"type": "human", "content": "world"}]}
|
||||
}
|
||||
mock_async_client = MagicMock()
|
||||
async_iter = MagicMock()
|
||||
async_iter.__aiter__.return_value = [
|
||||
StreamPart(event="values", data={"chunk": "data1"}),
|
||||
StreamPart(event="values", data={"chunk": "data2"}),
|
||||
StreamPart(
|
||||
event="values", data={"messages": [{"type": "human", "content": "world"}]}
|
||||
),
|
||||
]
|
||||
mock_async_client.runs.stream.return_value = async_iter
|
||||
|
||||
# call method / assertions
|
||||
remote_pregel = RemoteGraph(
|
||||
@@ -687,7 +697,7 @@ async def test_ainvoke():
|
||||
{"input": {"messages": [{"type": "human", "content": "hello"}]}}, config
|
||||
)
|
||||
|
||||
assert result == {"values": {"messages": [{"type": "human", "content": "world"}]}}
|
||||
assert result == {"messages": [{"type": "human", "content": "world"}]}
|
||||
|
||||
|
||||
@pytest.mark.skip("Unskip this test to manually test the LangGraph Cloud integration")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@langchain/langgraph-sdk",
|
||||
"version": "0.0.18",
|
||||
"version": "0.0.20",
|
||||
"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(
|
||||
@@ -143,6 +160,7 @@ export class CronsClient extends BaseClient {
|
||||
interrupt_after: payload?.interruptAfter,
|
||||
webhook: payload?.webhook,
|
||||
multitask_strategy: payload?.multitaskStrategy,
|
||||
if_not_exists: payload?.ifNotExists,
|
||||
};
|
||||
return this.fetch<Run>(`/threads/${threadId}/runs/crons`, {
|
||||
method: "POST",
|
||||
@@ -170,6 +188,7 @@ export class CronsClient extends BaseClient {
|
||||
interrupt_after: payload?.interruptAfter,
|
||||
webhook: payload?.webhook,
|
||||
multitask_strategy: payload?.multitaskStrategy,
|
||||
if_not_exists: payload?.ifNotExists,
|
||||
};
|
||||
return this.fetch<Run>(`/runs/crons`, {
|
||||
method: "POST",
|
||||
@@ -681,6 +700,7 @@ export class RunsClient extends BaseClient {
|
||||
on_completion: payload?.onCompletion,
|
||||
on_disconnect: payload?.onDisconnect,
|
||||
after_seconds: payload?.afterSeconds,
|
||||
if_not_exists: payload?.ifNotExists,
|
||||
};
|
||||
|
||||
const endpoint =
|
||||
@@ -689,6 +709,7 @@ export class RunsClient extends BaseClient {
|
||||
...this.prepareFetchOptions(endpoint, {
|
||||
method: "POST",
|
||||
json,
|
||||
timeoutMs: null,
|
||||
signal: payload?.signal,
|
||||
}),
|
||||
);
|
||||
@@ -761,6 +782,7 @@ export class RunsClient extends BaseClient {
|
||||
checkpoint_id: payload?.checkpointId,
|
||||
multitask_strategy: payload?.multitaskStrategy,
|
||||
after_seconds: payload?.afterSeconds,
|
||||
if_not_exists: payload?.ifNotExists,
|
||||
};
|
||||
return this.fetch<Run>(`/threads/${threadId}/runs`, {
|
||||
method: "POST",
|
||||
@@ -831,14 +853,31 @@ export class RunsClient extends BaseClient {
|
||||
on_completion: payload?.onCompletion,
|
||||
on_disconnect: payload?.onDisconnect,
|
||||
after_seconds: payload?.afterSeconds,
|
||||
if_not_exists: payload?.ifNotExists,
|
||||
};
|
||||
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 +950,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 +979,7 @@ export class RunsClient extends BaseClient {
|
||||
const response = await this.asyncCaller.fetch(
|
||||
...this.prepareFetchOptions(`/threads/${threadId}/runs/${runId}/stream`, {
|
||||
method: "GET",
|
||||
timeoutMs: null,
|
||||
signal,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -95,6 +95,11 @@ interface RunsInvokePayload {
|
||||
* Use to schedule future runs.
|
||||
*/
|
||||
afterSeconds?: number;
|
||||
|
||||
/**
|
||||
* Behavior if the specified run doesn't exist. Defaults to "reject".
|
||||
*/
|
||||
ifNotExists?: "create" | "reject";
|
||||
}
|
||||
|
||||
export interface RunsStreamPayload extends RunsInvokePayload {
|
||||
@@ -130,4 +135,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)
|
||||
@@ -603,7 +603,7 @@ class AssistantsClient:
|
||||
graph_id: The ID of the graph the assistant should use.
|
||||
The graph ID is normally set in your langgraph.json configuration. If None, assistant will keep pointing to same graph.
|
||||
config: Configuration to use for the graph.
|
||||
metadata: Metadata to add to assistant.
|
||||
metadata: Metadata to merge with existing assistant metadata.
|
||||
name: The new name for the assistant.
|
||||
|
||||
Returns:
|
||||
@@ -706,7 +706,10 @@ class AssistantsClient:
|
||||
"""List all versions of an assistant.
|
||||
|
||||
Args:
|
||||
assistant_id: The assistant ID to delete.
|
||||
assistant_id: The assistant ID to get versions for.
|
||||
metadata: Metadata to filter versions by. Exact match filter for each KV pair.
|
||||
limit: The maximum number of versions to return.
|
||||
offset: The number of versions to skip.
|
||||
|
||||
Returns:
|
||||
list[Assistant]: A list of assistants.
|
||||
@@ -839,7 +842,7 @@ class ThreadsClient:
|
||||
|
||||
Args:
|
||||
thread_id: ID of thread to update.
|
||||
metadata: Metadata to add/update to thread.
|
||||
metadata: Metadata to merge with existing thread metadata.
|
||||
|
||||
Returns:
|
||||
Thread: The created thread.
|
||||
@@ -885,10 +888,10 @@ class ThreadsClient:
|
||||
"""Search for threads.
|
||||
|
||||
Args:
|
||||
metadata: Thread metadata to search for.
|
||||
values: Thread values to search for.
|
||||
status: Status to search for.
|
||||
Must be one of 'idle', 'busy', or 'interrupted'.
|
||||
metadata: Thread metadata to filter on.
|
||||
values: State values to filter on.
|
||||
status: Thread status to filter on.
|
||||
Must be one of 'idle', 'busy', 'interrupted' or 'error'.
|
||||
limit: Limit on number of threads to return.
|
||||
offset: Offset in threads table to start search from.
|
||||
|
||||
@@ -951,7 +954,7 @@ class ThreadsClient:
|
||||
Args:
|
||||
thread_id: The ID of the thread to get the state of.
|
||||
checkpoint: The checkpoint to get the state of.
|
||||
subgraphs: Include subgraphs in the state.
|
||||
subgraphs: Include subgraphs states.
|
||||
|
||||
Returns:
|
||||
ThreadState: the thread of the state.
|
||||
@@ -1068,7 +1071,7 @@ class ThreadsClient:
|
||||
|
||||
Args:
|
||||
thread_id: The ID of the thread to update.
|
||||
values: The values to update to the state.
|
||||
values: The values to update the state with.
|
||||
as_node: Update the state as if this node had just executed.
|
||||
checkpoint: The checkpoint to update the state of.
|
||||
|
||||
@@ -1119,11 +1122,11 @@ class ThreadsClient:
|
||||
"""Get the state history of a thread.
|
||||
|
||||
Args:
|
||||
thread_id: The ID of the thread to get the state of.
|
||||
checkpoint: Get history for this subgraph. If empty defaults to root.
|
||||
limit: The maximum number of results to return.
|
||||
before: Get history before this checkpoint.
|
||||
metadata: Filter checkpoints by metadata.
|
||||
thread_id: The ID of the thread to get the state history for.
|
||||
checkpoint: Return states for this subgraph. If empty defaults to root.
|
||||
limit: The maximum number of states to return.
|
||||
before: Return states before this checkpoint.
|
||||
metadata: Filter states by metadata key-value pairs.
|
||||
|
||||
Returns:
|
||||
list[ThreadState]: the state history of the thread.
|
||||
@@ -1515,6 +1518,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
|
||||
@@ -1533,6 +1537,7 @@ class RunsClient:
|
||||
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(
|
||||
@@ -1553,6 +1558,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.
|
||||
|
||||
@@ -1645,9 +1651,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
|
||||
@@ -2260,7 +2276,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)
|
||||
@@ -2687,7 +2703,7 @@ class SyncAssistantsClient:
|
||||
graph_id: The ID of the graph the assistant should use.
|
||||
The graph ID is normally set in your langgraph.json configuration. If None, assistant will keep pointing to same graph.
|
||||
config: Configuration to use for the graph.
|
||||
metadata: Metadata to add to assistant.
|
||||
metadata: Metadata to merge with existing assistant metadata.
|
||||
name: The new name for the assistant.
|
||||
|
||||
Returns:
|
||||
@@ -2790,7 +2806,10 @@ class SyncAssistantsClient:
|
||||
"""List all versions of an assistant.
|
||||
|
||||
Args:
|
||||
assistant_id: The assistant ID to delete.
|
||||
assistant_id: The assistant ID to get versions for.
|
||||
metadata: Metadata to filter versions by. Exact match filter for each KV pair.
|
||||
limit: The maximum number of versions to return.
|
||||
offset: The number of versions to skip.
|
||||
|
||||
Returns:
|
||||
list[Assistant]: A list of assistants.
|
||||
@@ -2920,7 +2939,7 @@ class SyncThreadsClient:
|
||||
|
||||
Args:
|
||||
thread_id: ID of thread to update.
|
||||
metadata: Metadata to add/update to thread.
|
||||
metadata: Metadata to merge with existing thread metadata.
|
||||
|
||||
Returns:
|
||||
Thread: The created thread.
|
||||
@@ -2964,10 +2983,10 @@ class SyncThreadsClient:
|
||||
"""Search for threads.
|
||||
|
||||
Args:
|
||||
metadata: Thread metadata to search for.
|
||||
values: Thread values to search for.
|
||||
status: Status to search for.
|
||||
Must be one of 'idle', 'busy', or 'interrupted'.
|
||||
metadata: Thread metadata to filter on.
|
||||
values: State values to filter on.
|
||||
status: Thread status to filter on.
|
||||
Must be one of 'idle', 'busy', 'interrupted' or 'error'.
|
||||
limit: Limit on number of threads to return.
|
||||
offset: Offset in threads table to start search from.
|
||||
|
||||
@@ -3030,7 +3049,7 @@ class SyncThreadsClient:
|
||||
Args:
|
||||
thread_id: The ID of the thread to get the state of.
|
||||
checkpoint: The checkpoint to get the state of.
|
||||
subgraphs: Include subgraphs in the state.
|
||||
subgraphs: Include subgraphs states.
|
||||
|
||||
Returns:
|
||||
ThreadState: the thread of the state.
|
||||
@@ -3147,7 +3166,7 @@ class SyncThreadsClient:
|
||||
|
||||
Args:
|
||||
thread_id: The ID of the thread to update.
|
||||
values: The values to update to the state.
|
||||
values: The values to update the state with.
|
||||
as_node: Update the state as if this node had just executed.
|
||||
checkpoint: The checkpoint to update the state of.
|
||||
|
||||
@@ -3198,11 +3217,11 @@ class SyncThreadsClient:
|
||||
"""Get the state history of a thread.
|
||||
|
||||
Args:
|
||||
thread_id: The ID of the thread to get the state of.
|
||||
checkpoint: Get history for this subgraph. If empty defaults to root.
|
||||
limit: The maximum number of results to return.
|
||||
before: Get history before this checkpoint.
|
||||
metadata: Filter checkpoints by metadata.
|
||||
thread_id: The ID of the thread to get the state history for.
|
||||
checkpoint: Return states for this subgraph. If empty defaults to root.
|
||||
limit: The maximum number of states to return.
|
||||
before: Return states before this checkpoint.
|
||||
metadata: Filter states by metadata key-value pairs.
|
||||
|
||||
Returns:
|
||||
list[ThreadState]: the state history of the thread.
|
||||
|
||||
@@ -6,11 +6,10 @@ from typing import Any, Literal, NamedTuple, Optional, Sequence, TypedDict, Unio
|
||||
Json = Optional[dict[str, Any]]
|
||||
"""Represents a JSON-like structure, which can be None or a dictionary with string keys and any values."""
|
||||
|
||||
RunStatus = Literal["pending", "running", "error", "success", "timeout", "interrupted"]
|
||||
RunStatus = Literal["pending", "error", "success", "timeout", "interrupted"]
|
||||
"""
|
||||
Represents the status of a run:
|
||||
- "pending": The run is waiting to start.
|
||||
- "running": The run is currently in progress.
|
||||
- "error": The run encountered an error and stopped.
|
||||
- "success": The run completed successfully.
|
||||
- "timeout": The run exceeded its time limit.
|
||||
@@ -26,7 +25,9 @@ Represents the status of a thread:
|
||||
- "error": An exception occurred during task processing.
|
||||
"""
|
||||
|
||||
StreamMode = Literal["values", "messages", "updates", "events", "debug", "custom"]
|
||||
StreamMode = Literal[
|
||||
"values", "messages", "updates", "events", "debug", "custom", "messages-tuple"
|
||||
]
|
||||
"""
|
||||
Defines the mode of streaming:
|
||||
- "values": Stream only the values.
|
||||
@@ -120,7 +121,7 @@ class GraphSchema(TypedDict):
|
||||
graph_id: str
|
||||
"""The ID of the graph."""
|
||||
input_schema: Optional[dict]
|
||||
"""The schema for the graph state.
|
||||
"""The schema for the graph input.
|
||||
Missing if unable to generate JSON schema from graph."""
|
||||
output_schema: Optional[dict]
|
||||
"""The schema for the graph output.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.34"
|
||||
version = "0.1.35"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user