Compare commits

..
Author SHA1 Message Date
Ben Burns b9f26d28ea add missing build step 2025-02-13 23:03:58 -08:00
Ben Burns 06d4ba7fa0 bump prebuilt.md 2025-02-13 22:57:15 -08:00
Ben Burns 2dbdb36743 fix cassette hashing 2025-02-13 22:56:16 -08:00
Ben Burns 2b72fbd5de state-reducers.md WIP 2025-02-13 21:57:45 -08:00
Ben Burns c681545c97 make CodeQL happy, fix Makefile 2025-02-13 21:57:44 -08:00
Ben Burns 5ebdefba63 ts exec works - back out half-baked TS code snippet for now 2025-02-13 21:57:44 -08:00
Ben Burns 441923282c docs: get ts execution working in build pipeline 2025-02-13 21:57:37 -08:00
493 changed files with 33622 additions and 52850 deletions
+2 -1
View File
@@ -4,7 +4,7 @@ on:
workflow_call:
env:
POETRY_VERSION: "2.1.2"
POETRY_VERSION: "1.7.1"
jobs:
build:
@@ -71,3 +71,4 @@ jobs:
working-directory: libs/cli/js-examples
run: |
langgraph build -t langgraph-test-e
+7 -1
View File
@@ -9,7 +9,7 @@ on:
description: "From which folder this pipeline executes"
env:
POETRY_VERSION: "2.1.2"
POETRY_VERSION: "1.7.1"
# This env var allows us to get inline annotations when ruff has complaints.
RUFF_OUTPUT_FORMAT: github
@@ -50,6 +50,12 @@ jobs:
working-directory: ${{ inputs.working-directory }}
run: poetry check
- name: Check lock file
if: steps.changed-files.outputs.all
shell: bash
working-directory: ${{ inputs.working-directory }}
run: poetry lock --check
- name: Install dependencies
if: steps.changed-files.outputs.all
# Also installs dev/lint/test/typing dependencies, to ensure we have
+1 -1
View File
@@ -9,7 +9,7 @@ on:
description: "From which folder this pipeline executes"
env:
POETRY_VERSION: "2.1.2"
POETRY_VERSION: "1.7.1"
jobs:
build:
+1 -1
View File
@@ -4,7 +4,7 @@ on:
workflow_call:
env:
POETRY_VERSION: "2.1.2"
POETRY_VERSION: "1.7.1"
jobs:
build:
+1 -1
View File
@@ -9,7 +9,7 @@ on:
description: "From which folder this pipeline executes"
env:
POETRY_VERSION: "2.1.2"
POETRY_VERSION: "1.7.1"
PYTHON_VERSION: "3.10"
jobs:
+1 -1
View File
@@ -4,7 +4,7 @@ on:
workflow_call:
env:
POETRY_VERSION: "2.1.2"
POETRY_VERSION: "1.7.1"
jobs:
build:
+1 -1
View File
@@ -8,7 +8,7 @@ on:
- "libs/**"
env:
POETRY_VERSION: "2.1.2"
POETRY_VERSION: "1.7.1"
jobs:
benchmark:
+2 -2
View File
@@ -6,7 +6,7 @@ on:
- "libs/**"
env:
POETRY_VERSION: "2.1.2"
POETRY_VERSION: "1.7.1"
jobs:
benchmark:
@@ -43,7 +43,7 @@ jobs:
run: |
{
echo 'OUTPUT<<EOF'
make -s benchmark-fast
make -s benchmark
echo EOF
} >> "$GITHUB_OUTPUT"
- name: Compare benchmarks
+1 -80
View File
@@ -17,34 +17,10 @@ concurrency:
cancel-in-progress: true
env:
POETRY_VERSION: "2.1.2"
POETRY_VERSION: "1.7.1"
jobs:
changes:
runs-on: ubuntu-latest
outputs:
python: ${{ steps.filter.outputs.python }}
sdk-js: ${{ steps.filter.outputs.sdk-js }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
python:
- 'libs/langgraph/**'
- 'libs/sdk-py/**'
- 'libs/cli/**'
- 'libs/checkpoint/**'
- 'libs/checkpoint-sqlite/**'
- 'libs/checkpoint-postgres/**'
- 'libs/scheduler-kafka/**'
- 'libs/prebuilt/**'
sdk-js:
- 'libs/sdk-js/**'
lint:
needs: changes
name: cd ${{ matrix.working-directory }}
strategy:
matrix:
@@ -57,16 +33,13 @@ jobs:
"libs/checkpoint-sqlite",
"libs/checkpoint-postgres",
"libs/scheduler-kafka",
"libs/prebuilt",
]
if: needs.changes.outputs.python == 'true'
uses: ./.github/workflows/_lint.yml
with:
working-directory: ${{ matrix.working-directory }}
secrets: inherit
test:
needs: changes
name: cd ${{ matrix.working-directory }}
strategy:
matrix:
@@ -76,9 +49,7 @@ jobs:
"libs/checkpoint",
"libs/checkpoint-sqlite",
"libs/checkpoint-postgres",
"libs/prebuilt",
]
if: needs.changes.outputs.python == 'true'
uses: ./.github/workflows/_test.yml
with:
working-directory: ${{ matrix.working-directory }}
@@ -86,23 +57,17 @@ jobs:
# NOTE: we're testing langgraph separately because it requires a different matrix
test-langgraph:
needs: changes
if: needs.changes.outputs.python == 'true'
name: "cd libs/langgraph"
uses: ./.github/workflows/_test_langgraph.yml
secrets: inherit
# NOTE: we're testing scheduler-kafka separately because it requires a different matrix
test-scheduler-kafka:
needs: changes
if: needs.changes.outputs.python == 'true'
name: "cd libs/scheduler-kafka"
uses: ./.github/workflows/_test_scheduler_kafka.yml
secrets: inherit
check-sdk-methods:
needs: changes
if: needs.changes.outputs.python == 'true'
name: "Check SDK methods matching"
runs-on: ubuntu-latest
steps:
@@ -114,52 +79,12 @@ jobs:
- name: Run check_sdk_methods script
run: python .github/scripts/check_sdk_methods.py
check-schema:
needs: changes
if: needs.changes.outputs.python == 'true'
name: "Check CLI schema hasn't changed #${{ matrix.python-version }}"
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.11"
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
uses: "./.github/actions/poetry_setup"
with:
python-version: "3.11"
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: schema-check-cli
- name: Install CLI dependencies
run: |
cd libs/cli
poetry install
- name: Generate schema and check for changes
run: |
cd libs/cli
# Create a temporary copy of the current schema
cp schemas/schema.json schemas/schema.current.json
# Generate new schema
poetry run python generate_schema.py
# Compare the new schema with the original
if ! diff -q schemas/schema.json schemas/schema.current.json > /dev/null; then
echo "Error: Langgraph.json configuration schema has changed. Please run 'poetry run python generate_schema.py' in the libs/cli directory and commit the changes."
diff schemas/schema.json schemas/schema.current.json
exit 1
fi
echo "Schema check passed - no changes detected"
integration-test:
needs: changes
if: needs.changes.outputs.python == 'true'
name: CLI integration test
uses: ./.github/workflows/_integration_test.yml
secrets: inherit
lint-js:
needs: changes
if: needs.changes.outputs.sdk-js == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
@@ -184,8 +109,6 @@ jobs:
run: yarn build
test-js:
needs: changes
if: needs.changes.outputs.sdk-js == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
@@ -216,8 +139,6 @@ jobs:
test,
test-langgraph,
test-scheduler-kafka,
check-sdk-methods,
check-schema,
integration-test,
test-js,
]
+29 -14
View File
@@ -10,7 +10,7 @@ on:
workflow_dispatch:
env:
POETRY_VERSION: "2.1.2"
POETRY_VERSION: "1.7.1"
permissions:
contents: read
@@ -56,6 +56,12 @@ jobs:
with:
fetch-depth: 0
- uses: actions/checkout@v4
with:
repository: langchain-ai/langchainjs
token: ${{ secrets.MKDOCS_GITHUB_TOKEN }}
path: docs/langchainjs
- name: Set up Python + Poetry ${{ env.POETRY_VERSION }}
uses: "./.github/actions/poetry_setup"
with:
@@ -63,16 +69,36 @@ jobs:
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: docs
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "22"
cache: "yarn"
cache-dependency-path: docs/yarn.lock
- name: Install dependencies
run: |
cd langchainjs && yarn && yarn build && cd ..
yarn
poetry install --with test --with docs --no-root
poetry run pip install -U \
pytest \
pytest-check-links \
GitPython \
"git+https://github.com/benjamincburns/markdown-exec.git@cc0d39d737e5ffd4b83d23cd8729d7ea16e363c8"
# we run this installation only for internal PRs
# as GITHUB_TOKEN is not available for PRs from outside contributors
if [ -n "${GITHUB_TOKEN}" ]; then
poetry run pip install "git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
fi
poetry run jupyter kernelspec list
poetry run python3 -m ipykernel install --user --name=python3
npm install -g tslab
poetry run tslab install --python=python3
poetry run jupyter kernelspec list
- name: Run unit tests
# Run unit tests on the docs build pipeline
run: make tests
@@ -83,14 +109,7 @@ jobs:
- name: Build llms-text
run: make llms-text
- name: Build site
run: |
# If this is main branch, then we want to download stats. we do this
# with the env variable DOWNLOAD_STATS=true
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
DOWNLOAD_STATS=true make build-docs
else
make build-docs
fi
run: make build-docs
env:
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.MKDOCS_GIT_COMMITTERS_APIKEY }}
OPENAI_API_KEY: sf-proj-1234567890 # fake placeholder, shouldn't actually be used
@@ -99,13 +118,12 @@ jobs:
env:
LANGCHAIN_API_KEY: test
run: |
if [ "${{ github.event_name }}" == "schedule" ]; then
if [ "${{ github.event_name }}" == "schedule" ] || [ "${{ github.event_name }}" == "workflow_dispatch" ] || ([ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]); then
echo "Running link check on all HTML files matching notebooks in docs directory..."
poetry run pytest -v \
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
--check-links-ignore "https://academy\.langchain\.com/.*" \
--check-links-ignore "https://x.com/.*" \
--check-links-ignore "https://twitter.com/.*" \
--check-links-ignore "https://github\.com/.*" \
--check-links-ignore "http://localhost:8123/.*" \
--check-links-ignore "http://localhost:2024.*" \
@@ -115,7 +133,6 @@ jobs:
--check-links-ignore "https://openai\.com/.*" \
--check-links-ignore "https://www\.uber\.com/.*" \
--check-links-ignore "https://pepy\.tech/.*" \
--check-links-ignore "docs/docs/static/wordmark_*" \
--check-links $(find site -name "index.html" | grep -v 'storm/index.html')
else
@@ -133,10 +150,8 @@ jobs:
--check-links-ignore "http://localhost:2024.*" \
--check-links-ignore "http://127.0.0.1:.*" \
--check-links-ignore "https://x.com/.*" \
--check-links-ignore "https://twitter.com/.*" \
--check-links-ignore "https://github\.com/.*" \
--check-links-ignore "/.*\.(ipynb|html)$" \
--check-links-ignore "docs/docs/static/wordmark_*" \
--check-links ${CHANGED_FILES} \
|| ([ $? = 5 ] && exit 0 || exit $?)
else
+6 -6
View File
@@ -12,7 +12,7 @@ on:
workflow_dispatch:
env:
POETRY_VERSION: "2.1.2"
POETRY_VERSION: "1.7.1"
jobs:
markdown-link-check:
@@ -42,8 +42,8 @@ jobs:
- name: Check README.md is in sync
run: |
if ! diff -q README.md libs/langgraph/README.md >/dev/null; then
echo "README.md is out of sync with libs/langgraph/README.md"
diff -C 3 README.md libs/langgraph/README.md
exit 1
fi
if ! diff -q README.md libs/langgraph/README.md >/dev/null; then
echo "README.md is out of sync with libs/langgraph/README.md"
diff -C 3 README.md libs/langgraph/README.md
exit 1
fi
+2 -6
View File
@@ -10,7 +10,7 @@ on:
env:
PYTHON_VERSION: "3.11"
POETRY_VERSION: "2.1.2"
POETRY_VERSION: "1.7.1"
jobs:
build:
@@ -195,11 +195,7 @@ jobs:
"$PKG_NAME==$VERSION" \
)
if [[ "$PKG_NAME" == *prebuilt* ]]; then
poetry run pip install langgraph
fi
if [[ "$PKG_NAME" == *checkpoint* || "$PKG_NAME" == *prebuilt* ]]; then
if [[ "$PKG_NAME" == *checkpoint* ]]; then
# since checkpoint packages are namespace packages, import them with . convention
# i.e. import langgraph.checkpoint or langgraph.checkpoint.sqlite
IMPORT_NAME="$(echo "$PKG_NAME" | sed s/-/./g)"
+10 -10
View File
@@ -9,7 +9,7 @@ on:
type: string
description: "JSON string of changed files"
schedule:
- cron: "0 13 * * *"
- cron: '0 13 * * *'
defaults:
run:
@@ -30,12 +30,12 @@ jobs:
uses: "./.github/actions/poetry_setup"
with:
python-version: 3.11
poetry-version: 2.1.2
poetry-version: 1.7.1
cache-key: test-langgraph-notebooks
- name: Install dependencies
run: |
poetry install --with test --no-root
poetry install --with test
poetry run pip install jupyter
- name: Start services
@@ -57,13 +57,13 @@ jobs:
env:
# these won't actually be used because of the VCR cassettes
# but need to set them to avoid triggering getpass()
OPENAI_API_KEY: "very-secret-key"
ANTHROPIC_API_KEY: "very-secret-key"
TAVILY_API_KEY: "very-secret-key"
LANGSMITH_API_KEY: "very-secret-key"
NOMIC_API_KEY: "very-secret-key"
COHERE_API_KEY: "very-secret-key"
FIREWORKS_API_KEY: "very-secret-key"
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
NOMIC_API_KEY: ${{ secrets.NOMIC_API_KEY }}
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "schedule" ]; then
echo "Running all notebooks"
+29
View File
@@ -0,0 +1,29 @@
name: Check File Size
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
jobs:
file-size-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v44
- name: Filter by size
# TODO: roll back the web voyager hack
run: |
large_added_files=$(find ${{ steps.changed-files.outputs.added_files }} -maxdepth 0 -size +1M | grep -v "web_voyager" || true)
if [ -n "$large_added_files" ]; then
echo "Large files added: $large_added_files"
echo "# Large files added:" >> $GITHUB_STEP_SUMMARY
echo "$large_added_files" >> $GITHUB_STEP_SUMMARY
exit 1
fi
-1
View File
@@ -180,4 +180,3 @@ Chinook.db
.vercel
.turbo
.editorconfig
+297 -48
View File
@@ -1,90 +1,339 @@
<picture class="github-only">
<source media="(prefers-color-scheme: light)" srcset="https://langchain-ai.github.io/langgraph/static/wordmark_dark.svg">
<source media="(prefers-color-scheme: dark)" srcset="https://langchain-ai.github.io/langgraph/static/wordmark_light.svg">
<img alt="LangGraph Logo" src="https://langchain-ai.github.io/langgraph/static/wordmark_dark.svg" width="80%">
</picture>
# 🦜🕸️LangGraph
<div>
<br>
</div>
[![Version](https://img.shields.io/pypi/v/langgraph.svg)](https://pypi.org/project/langgraph/)
![Version](https://img.shields.io/pypi/v/langgraph)
[![Downloads](https://static.pepy.tech/badge/langgraph/month)](https://pepy.tech/project/langgraph)
[![Open Issues](https://img.shields.io/github/issues-raw/langchain-ai/langgraph)](https://github.com/langchain-ai/langgraph/issues)
[![Docs](https://img.shields.io/badge/docs-latest-blue)](https://langchain-ai.github.io/langgraph/)
⚡ Building language agents as graphs ⚡
> [!NOTE]
> Looking for the JS version? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://langchain-ai.github.io/langgraphjs/).
LangGraph — used by Replit, Uber, LinkedIn, GitLab and more — is a low-level orchestration framework for building controllable agents. While langchain provides integrations and composable components to streamline LLM application development, the LangGraph library enables agent orchestration — offering customizable architectures, long-term memory, and human-in-the-loop to reliably handle complex tasks.
## Overview
```bash
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building
stateful, multi-actor applications with LLMs, used to create agent and multi-agent
workflows. Check out an introductory tutorial [here](https://langchain-ai.github.io/langgraph/tutorials/introduction/).
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
### Why use LangGraph?
LangGraph powers [production-grade agents](https://www.langchain.com/built-with-langgraph), trusted by Linkedin, Uber, Klarna, GitLab, and many more. LangGraph provides fine-grained control over both the flow and state of your agent applications. It implements a central [persistence layer](https://langchain-ai.github.io/langgraph/concepts/persistence/), enabling features that are common to most agent architectures:
- **Memory**: LangGraph persists arbitrary aspects of your application's state,
supporting memory of conversations and other updates within and across user
interactions;
- **Human-in-the-loop**: Because state is checkpointed, execution can be interrupted
and resumed, allowing for decisions, validation, and corrections at key stages via
human input.
Standardizing these components allows individuals and teams to focus on the behavior
of their agent, instead of its supporting infrastructure.
Through [LangGraph Platform](#langgraph-platform), LangGraph also provides tooling for
the development, deployment, debugging, and monitoring of your applications.
LangGraph integrates seamlessly with
[LangChain](https://python.langchain.com/docs/introduction/) and
[LangSmith](https://docs.smith.langchain.com/) (but does not require them).
To learn more about LangGraph, check out our first LangChain Academy
course, *Introduction to LangGraph*, available for free
[here](https://academy.langchain.com/courses/intro-to-langgraph).
### LangGraph Platform
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform) is infrastructure for deploying LangGraph agents. It is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework. The LangGraph Platform consists of several components that work together to support the development, deployment, debugging, and monitoring of LangGraph applications: [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server) (APIs), [LangGraph SDKs](https://langchain-ai.github.io/langgraph/concepts/sdk) (clients for the APIs), [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli) (command line tool for building the server), and [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio) (UI/debugger).
See deployment options [here](https://langchain-ai.github.io/langgraph/concepts/deployment_options/)
(includes a free tier).
Here are some common issues that arise in complex deployments, which LangGraph Platform addresses:
- **Streaming support**: LangGraph Server provides [multiple streaming modes](https://langchain-ai.github.io/langgraph/concepts/streaming) optimized for various application needs
- **Background runs**: Runs agents asynchronously in the background
- **Support for long running agents**: Infrastructure that can handle long running processes
- **[Double texting](https://langchain-ai.github.io/langgraph/concepts/double_texting)**: Handle the case where you get two messages from the user before the agent can respond
- **Handle burstiness**: Task queue for ensuring requests are handled consistently without loss, even under heavy loads
## Installation
```shell
pip install -U langgraph
```
To learn more about how to use LangGraph, check out [the docs](https://langchain-ai.github.io/langgraph/). We show a simple example below of how to create a ReAct agent.
## Example
Let's build a tool-calling [ReAct-style](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#react-implementation) agent that uses a search tool!
```shell
pip install langchain-anthropic
```
```shell
export ANTHROPIC_API_KEY=sk-...
```
Optionally, we can set up [LangSmith](https://docs.smith.langchain.com/) for best-in-class observability.
```shell
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=lsv2_sk_...
```
The simplest way to create a tool-calling agent in LangGraph is to use `create_react_agent`:
<details open>
<summary>High-level implementation</summary>
```python
# This code depends on pip install langchain[anthropic]
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
# Define the tools for the agent to use
@tool
def search(query: str):
"""Call to surf the web."""
# This is a placeholder, but don't tell the LLM that...
if "sf" in query.lower() or "san francisco" in query.lower():
return "It's 60 degrees and foggy."
return "It's 90 degrees and sunny."
agent = create_react_agent("anthropic:claude-3-7-sonnet-latest", tools=[search])
agent.invoke(
{"messages": [{"role": "user", "content": "what is the weather in sf"}]}
tools = [search]
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0)
# Initialize memory to persist state between graph runs
checkpointer = MemorySaver()
app = create_react_agent(model, tools, checkpointer=checkpointer)
# Use the agent
final_state = app.invoke(
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
```
"Based on the search results, I can tell you that the current weather in San Francisco is:\n\nTemperature: 60 degrees Fahrenheit\nConditions: Foggy\n\nSan Francisco is known for its microclimates and frequent fog, especially during the summer months. The temperature of 60°F (about 15.5°C) is quite typical for the city, which tends to have mild temperatures year-round. The fog, often referred to as "Karl the Fog" by locals, is a characteristic feature of San Francisco\'s weather, particularly in the mornings and evenings.\n\nIs there anything else you\'d like to know about the weather in San Francisco or any other location?"
```
Now when we pass the same <code>"thread_id"</code>, the conversation context is retained via the saved state (i.e. stored list of messages)
```python
final_state = app.invoke(
{"messages": [{"role": "user", "content": "what about ny"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
```
"Based on the search results, I can tell you that the current weather in New York City is:\n\nTemperature: 90 degrees Fahrenheit (approximately 32.2 degrees Celsius)\nConditions: Sunny\n\nThis weather is quite different from what we just saw in San Francisco. New York is experiencing much warmer temperatures right now. Here are a few points to note:\n\n1. The temperature of 90°F is quite hot, typical of summer weather in New York City.\n2. The sunny conditions suggest clear skies, which is great for outdoor activities but also means it might feel even hotter due to direct sunlight.\n3. This kind of weather in New York often comes with high humidity, which can make it feel even warmer than the actual temperature suggests.\n\nIt's interesting to see the stark contrast between San Francisco's mild, foggy weather and New York's hot, sunny conditions. This difference illustrates how varied weather can be across different parts of the United States, even on the same day.\n\nIs there anything else you'd like to know about the weather in New York or any other location?"
```
</details>
> [!TIP]
> Check out [this guide](https://langchain-ai.github.io/langgraph/tutorials/workflows/) that walks through implementing common patterns (workflows and agents) in LangGraph.
> LangGraph is a **low-level** framework that allows you to implement any custom agent
architectures. Click on the low-level implementation below to see how to implement a
tool-calling agent from scratch.
## Why use LangGraph?
<details>
<summary>Low-level implementation</summary>
LangGraph is built for developers who want to build powerful, adaptable AI agents. Developers choose LangGraph for:
```python
from typing import Literal
- **Reliability and controllability.** Steer agent actions with moderation checks and human-in-the-loop approvals. LangGraph persists context for long-running workflows, keeping your agents on course.
- **Low-level and extensible.** Build custom agents with fully descriptive, low-level primitives – free from rigid abstractions that limit customization. Design scalable multi-agent systems, with each agent serving a specific role tailored to your use case.
- **First-class streaming support.** With token-by-token streaming and streaming of intermediate steps, LangGraph gives users clear visibility into agent reasoning and actions as they unfold in real time.
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, StateGraph, MessagesState
from langgraph.prebuilt import ToolNode
LangGraph is trusted in production and powering agents for companies like:
- [Klarna](https://blog.langchain.dev/customers-klarna/): Customer support bot for 85 million active users
- [Elastic](https://www.elastic.co/blog/elastic-security-generative-ai-features): Security AI assistant for threat detection
- [Uber](https://dpe.org/sessions/ty-smith-adam-huda/this-year-in-ubers-ai-driven-developer-productivity-revolution/): Automated unit test generation
- [Replit](https://www.langchain.com/breakoutagents/replit): Code generation
- And many more ([see list here](https://www.langchain.com/built-with-langgraph))
# Define the tools for the agent to use
@tool
def search(query: str):
"""Call to surf the web."""
# This is a placeholder, but don't tell the LLM that...
if "sf" in query.lower() or "san francisco" in query.lower():
return "It's 60 degrees and foggy."
return "It's 90 degrees and sunny."
## LangGraph’s ecosystem
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
tools = [search]
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
tool_node = ToolNode(tools)
## Pairing with LangGraph Platform
model = ChatAnthropic(model="claude-3-5-sonnet-latest", temperature=0).bind_tools(tools)
While LangGraph is our open-source agent orchestration framework, enterprises that need scalable agent deployment can benefit from [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/).
# Define the function that determines whether to continue or not
def should_continue(state: MessagesState) -> Literal["tools", END]:
messages = state['messages']
last_message = messages[-1]
# If the LLM makes a tool call, then we route to the "tools" node
if last_message.tool_calls:
return "tools"
# Otherwise, we stop (reply to the user)
return END
LangGraph Platform can help engineering teams:
- **Accelerate agent development**: Quickly create agent UXs with configurable templates and [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/) for visualizing and debugging agent interactions.
- **Deploy seamlessly**: We handle the complexity of deploying your agent. LangGraph Platform includes robust APIs for memory, threads, and cron jobs plus auto-scaling task queues & servers.
- **Centralize agent management & reusability**: Discover, reuse, and manage agents across the organization. Business users can also modify agents without coding.
# Define the function that calls the model
def call_model(state: MessagesState):
messages = state['messages']
response = model.invoke(messages)
# We return a list, because this will get added to the existing list
return {"messages": [response]}
## Additional resources
- [LangChain Academy](https://academy.langchain.com/courses/intro-to-langgraph): Learn the basics of LangGraph in our free, structured course.
- [Tutorials](https://langchain-ai.github.io/langgraph/tutorials/): Simple walkthroughs with guided examples on getting started with LangGraph.
- [Templates](https://langchain-ai.github.io/langgraph/concepts/template_applications/): Pre-built reference apps for common agentic workflows (e.g. ReAct agent, memory, retrieval etc.) that can be cloned and adapted.
- [How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
- [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
- [Built with LangGraph](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship powerful, production-ready AI applications.
# Define a new graph
workflow = StateGraph(MessagesState)
## Acknowledgements
# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
workflow.add_node("tools", tool_node)
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
# Set the entrypoint as `agent`
# This means that this node is the first one called
workflow.add_edge(START, "agent")
# We now add a conditional edge
workflow.add_conditional_edges(
# First, we define the start node. We use `agent`.
# This means these are the edges taken after the `agent` node is called.
"agent",
# Next, we pass in the function that will determine which node is called next.
should_continue,
)
# We now add a normal edge from `tools` to `agent`.
# This means that after `tools` is called, `agent` node is called next.
workflow.add_edge("tools", 'agent')
# Initialize memory to persist state between graph runs
checkpointer = MemorySaver()
# Finally, we compile it!
# This compiles it into a LangChain Runnable,
# meaning you can use it as you would any other runnable.
# Note that we're (optionally) passing the memory when compiling the graph
app = workflow.compile(checkpointer=checkpointer)
# Use the agent
final_state = app.invoke(
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
config={"configurable": {"thread_id": 42}}
)
final_state["messages"][-1].content
```
<b>Step-by-step Breakdown</b>:
<details>
<summary>Initialize the model and tools.</summary>
<ul>
<li>
We use <code>ChatAnthropic</code> as our LLM. <strong>NOTE:</strong> we need to make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the <code>.bind_tools()</code> method.
</li>
<li>
We define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that <a href="https://python.langchain.com/docs/how_to/custom_tools/">here</a>.
</li>
</ul>
</details>
<details>
<summary>Initialize graph with state.</summary>
<ul>
<li>We initialize graph (<code>StateGraph</code>) by passing state schema (in our case <code>MessagesState</code>)</li>
<li><code>MessagesState</code> is a prebuilt state schema that has one attribute -- a list of LangChain <code>Message</code> objects, as well as logic for merging the updates from each node into the state.</li>
</ul>
</details>
<details>
<summary>Define graph nodes.</summary>
There are two main nodes we need:
<ul>
<li>The <code>agent</code> node: responsible for deciding what (if any) actions to take.</li>
<li>The <code>tools</code> node that invokes tools: if the agent decides to take an action, this node will then execute that action.</li>
</ul>
</details>
<details>
<summary>Define entry point and graph edges.</summary>
First, we need to set the entry point for graph execution - <code>agent</code> node.
Then we define one normal and one conditional edge. Conditional edge means that the destination depends on the contents of the graph's state (<code>MessagesState</code>). In our case, the destination is not known until the agent (LLM) decides.
<ul>
<li>Conditional edge: after the agent is called, we should either:
<ul>
<li>a. Run tools if the agent said to take an action, OR</li>
<li>b. Finish (respond to the user) if the agent did not ask to run tools</li>
</ul>
</li>
<li>Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next</li>
</ul>
</details>
<details>
<summary>Compile the graph.</summary>
<ul>
<li>
When we compile the graph, we turn it into a LangChain
<a href="https://python.langchain.com/docs/concepts/runnables/">Runnable</a>,
which automatically enables calling <code>.invoke()</code>, <code>.stream()</code> and <code>.batch()</code>
with your inputs
</li>
<li>
We can also optionally pass checkpointer object for persisting state between graph runs, and enabling memory,
human-in-the-loop workflows, time travel and more. In our case we use <code>MemorySaver</code> -
a simple in-memory checkpointer
</li>
</ul>
</details>
<details>
<summary>Execute the graph.</summary>
<ol>
<li>LangGraph adds the input message to the internal state, then passes the state to the entrypoint node, <code>"agent"</code>.</li>
<li>The <code>"agent"</code> node executes, invoking the chat model.</li>
<li>The chat model returns an <code>AIMessage</code>. LangGraph adds this to the state.</li>
<li>Graph cycles the following steps until there are no more <code>tool_calls</code> on <code>AIMessage</code>:
<ul>
<li>If <code>AIMessage</code> has <code>tool_calls</code>, <code>"tools"</code> node executes</li>
<li>The <code>"agent"</code> node executes again and returns <code>AIMessage</code></li>
</ul>
</li>
<li>Execution progresses to the special <code>END</code> value and outputs the final state. And as a result, we get a list of all our chat messages as output.</li>
</ol>
</details>
</details>
## Documentation
* [Tutorials](https://langchain-ai.github.io/langgraph/tutorials/): Learn to build with LangGraph through guided examples.
* [How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): Accomplish specific things within LangGraph, from streaming, to adding memory & persistence, to common design patterns (branching, subgraphs, etc.), these are the place to go if you want to copy and run a specific code snippet.
* [Conceptual Guides](https://langchain-ai.github.io/langgraph/concepts/high_level/): In-depth explanations of the key concepts and principles behind LangGraph, such as nodes, edges, state and more.
* [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Review important classes and methods, simple examples of how to use the graph and checkpointing APIs, higher-level prebuilt components and more.
* [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform): LangGraph Platform is a commercial solution for deploying agentic applications in production, built on the open-source LangGraph framework.
## Resources
* [Built with LangGraph](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship powerful, production-ready AI applications.
## Contributing
For more information on how to contribute, see [here](https://github.com/langchain-ai/langgraph/blob/main/CONTRIBUTING.md).
+873
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.5.1.cjs
+25 -13
View File
@@ -10,18 +10,25 @@ build-prebuilt:
# Use to create an update to date prebuilt page.
# Looks up download stats for each of the prebuilt packages and
# generates the final prebuilt page.
@if [ "$(DOWNLOAD_STATS)" = "true" ]; then \
set -x; \
poetry run python -m _scripts.third_party_page.get_download_stats stats.yml; \
set +x; \
else \
set -x; \
poetry run python -m _scripts.third_party_page.get_download_stats --fake stats.yml; \
set +x; \
fi
poetry run python -m _scripts.third_party_page.create_third_party_page stats.yml docs/agents/prebuilt.md --language python
poetry run python -m _scripts.third_party_page.get_download_stats stats.yml
poetry run python -m _scripts.third_party_page.create_third_party_page stats.yml docs/prebuilt.md --language python
build-docs: build-typedoc build-prebuilt
grab-langgraphjs:
if [ -d "langgraphjs" ]; then \
if [ ! -d "langgraphjs/.git" ]; then \
rm -rf langgraphjs; \
fi \
fi
if [ ! -d "langgraphjs" ]; then \
git clone https://github.com/langchain-ai/langgraphjs.git; \
else \
cd langgraphjs && git checkout main && git pull; \
fi
cd langgraphjs && yarn
cd langgraphjs && yarn build
yarn
build-docs: build-typedoc build-prebuilt grab-langgraphjs
poetry run python -m mkdocs build --clean -f mkdocs.yml --strict
llms-text:
@@ -34,9 +41,14 @@ install-vercel-deps:
# don't use vercel's python - it wasn't compiled with sqlite support, and it fails when installing ipython's kernel
poetry env use /usr/bin/python3.11
poetry install --with docs --with test --no-root
poetry run pip install "git+https://github.com/benjamincburns/markdown-exec.git@cc0d39d737e5ffd4b83d23cd8729d7ea16e363c8"
poetry run python3 -m ipykernel install --name=python3
npm install -g tslab
poetry run tslab install --python=python3
poetry run jupyter kernelspec list
tests:
# Run unit tests
# RUn unit tests
poetry run pytest tests/unit_tests
@@ -47,7 +59,7 @@ vercel-build-docs: install-vercel-deps
serve-clean-docs: clean-docs
poetry run python -m mkdocs serve -c -f mkdocs.yml --strict -w ../libs/langgraph
serve-docs: build-typedoc
serve-docs: build-typedoc grab-langgraphjs
poetry run python -m mkdocs serve -f mkdocs.yml -w ../libs/langgraph -w ../libs/checkpoint -w ../libs/sdk-py --dirty
clean-docs:
+1 -3
View File
@@ -14,8 +14,6 @@ To run the documentation server locally you can run:
make serve-docs
```
This will start the documentation server on [http://127.0.0.1:8000/langgraph/](http://127.0.0.1:8000/langgraph/).
## Execute notebooks
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GHA, you can run:
@@ -58,4 +56,4 @@ To delete cassettes for a notebook, you can run:
```bash
rm cassettes/<notebook_name>*
```
```
@@ -0,0 +1,77 @@
import nock, { Definition } from "nock";
import msgpack from "msgpack-lite";
import zlib from "node:zlib";
import fs from "node:fs/promises";
import { Buffer } from "node:buffer";
// deno style imports here because we're running this in the deno jupyter kernel
interface NockCassetteData {
hash: string;
entries: Definition[];
}
// Utility functions for compression & serialization
function compressData(data: NockCassetteData, compressionLevel = 9): string {
const packed = msgpack.encode(data);
const compressed = zlib.deflateSync(packed, { level: compressionLevel });
return compressed.toString("base64");
}
function decompressData(compressedString: string): NockCassetteData {
const decoded = Buffer.from(compressedString, "base64");
const decompressed = zlib.inflateSync(decoded);
return msgpack.decode(decompressed) as NockCassetteData;
}
class HashedCassette {
hash: string;
private recording = true;
constructor(
private readonly cassettePath: string,
hash: string
) {
this.hash = hash;
}
async enter() {
try {
const rawCassette = await fs.readFile(this.cassettePath, "utf-8");
const data = decompressData(rawCassette);
if (data.hash === this.hash) {
this.recording = false;
nock.disableNetConnect();
nock.define(data.entries);
return;
}
} catch (error) {
if (error instanceof Error && error.message.includes("ENOENT")) {
this.recording = true;
} else {
throw error;
}
}
nock.recorder.rec({
dont_print: true,
output_objects: true,
});
}
async exit() {
if (this.recording) {
const entries = nock.recorder.play() as Definition[];
const data = {
hash: this.hash,
entries,
};
const compressed = compressData(data);
await fs.writeFile(this.cassettePath, compressed);
} else {
nock.enableNetConnect();
nock.restore();
nock.cleanAll();
}
}
}
+107
View File
@@ -0,0 +1,107 @@
import base64
import os
import zlib
from types import TracebackType
from typing import Optional, Any, Type
import msgpack
import vcr
os.environ.pop("LANGCHAIN_TRACING_V2", None)
custom_vcr = vcr.VCR()
def compress_data(data: Any, compression_level: int = 9) -> str:
packed = msgpack.packb(data, use_bin_type=True)
compressed = zlib.compress(packed, level=compression_level)
return base64.b64encode(compressed).decode("utf-8")
def decompress_data(compressed_string: str) -> Any:
decoded = base64.b64decode(compressed_string)
decompressed = zlib.decompress(decoded)
return msgpack.unpackb(decompressed, raw=False)
class AdvancedCompressedSerializer:
def serialize(self, cassette_dict: Any) -> str:
return compress_data(cassette_dict)
def deserialize(self, cassette_string: str) -> Any:
return decompress_data(cassette_string)
custom_vcr.register_serializer("advanced_compressed", AdvancedCompressedSerializer())
custom_vcr.serializer = "advanced_compressed"
class HashedCassette:
def __init__(self, cassette_path: str, hash_value: str) -> None:
"""A context manager for using VCR cassettes with an embedded hash value.
Args:
cassette_path (str): The file path of the cassette (independent of hash).
hash_value (str): The expected hash value (e.g. a uuid string).
This class provides a context manager for using VCR cassettes with an embedded hash value.
The hash value is used to ensure that the cassette matches the expected state, and if not,
the cassette is removed or updated with the new hash value.
"""
self.cassette_path: str = cassette_path
self.hash_value: str = hash_value
self.vcr: vcr.VCR = custom_vcr
self.cassette_context: Optional[Any] = None
self.exited: bool = False
def __enter__(self) -> Any:
self.exited: bool = False
# Get the serializer instance from the VCR instance.
serializer = self.vcr.serializers[self.vcr.serializer]
# If the cassette file exists, check its embedded hash.
if os.path.exists(self.cassette_path):
with open(self.cassette_path, "r") as f:
content = f.read()
try:
cassette_data = serializer.deserialize(content)
except Exception:
os.remove(self.cassette_path)
else:
existing_hash = cassette_data.get("cassette_hash")
if existing_hash != self.hash_value:
os.remove(self.cassette_path)
# Now enter the VCR cassette context.
self.cassette_context = custom_vcr.use_cassette(
self.cassette_path,
filter_headers=["x-api-key", "authorization"],
record_mode="once",
serializer="advanced_compressed",
)
return self.cassette_context.__enter__()
def __exit__(
self,
exc_type: Optional[Type[BaseException]] = None,
exc_val: Optional[BaseException] = None,
exc_tb: Optional[TracebackType] = None,
) -> Optional[bool]:
if self.exited:
return
self.exited = True
# Exit the VCR cassette context.
result = self.cassette_context.__exit__(exc_type, exc_val, exc_tb)
serializer = self.vcr.serializers[self.vcr.serializer]
# If a cassette was recorded (or updated), open and update its hash.
if os.path.exists(self.cassette_path):
with open(self.cassette_path, "r") as f:
content = f.read()
try:
cassette_data = serializer.deserialize(content)
except Exception:
return result
# Update the cassette data with the expected hash.
if cassette_data.get("cassette_hash") != self.hash_value:
cassette_data["cassette_hash"] = self.hash_value
serialized_data = serializer.serialize(cassette_data)
with open(self.cassette_path, "w") as f:
f.write(serialized_data)
return result
+195 -141
View File
@@ -1,9 +1,12 @@
import ast
import importlib
from importlib.machinery import ModuleSpec
import importlib.util
import inspect
import logging
import re
from functools import lru_cache
from typing import List, Optional
import sys
from typing import List, Literal, Optional
from typing_extensions import TypedDict
@@ -22,12 +25,6 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
"create_react_agent",
"prebuilt",
),
(
[],
"langgraph.prebuilt.chat_agent_executor",
"AgentState",
"prebuilt",
),
(["langgraph.prebuilt"], "langgraph.prebuilt.tool_node", "ToolNode", "prebuilt"),
(
["langgraph.prebuilt"],
@@ -45,42 +42,26 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
(["langgraph.graph"], "langgraph.graph.message", "add_messages", "graphs"),
(["langgraph.graph"], "langgraph.graph.state", "StateGraph", "graphs"),
(["langgraph.graph"], "langgraph.graph.state", "CompiledStateGraph", "graphs"),
([], "langgraph.types", "StreamMode", "types"),
(["langgraph.graph"], "langgraph.constants", "START", "constants"),
(["langgraph.graph"], "langgraph.constants", "END", "constants"),
(["langgraph.constants"], "langgraph.types", "Send", "types"),
(["langgraph.constants"], "langgraph.types", "Interrupt", "types"),
(["langgraph.constants"], "langgraph.types", "interrupt", "types"),
(["langgraph.constants"], "langgraph.types", "Command", "types"),
(["langgraph.config"], "langgraph.config", "get_stream_writer", "config"),
(["langgraph.config"], "langgraph.config", "get_store", "config"),
(["langgraph.func"], "langgraph.func", "entrypoint", "func"),
(["langgraph.func"], "langgraph.func", "task", "func"),
(["langgraph.types"], "langgraph.types", "RetryPolicy", "types"),
(["langgraph.types"], "langgraph.types", "StreamMode", "types"),
(["langgraph.types"], "langgraph.types", "StreamWriter", "types"),
([], "langgraph.types", "RetryPolicy", "types"),
([], "langgraph.checkpoint.base", "Checkpoint", "checkpoints"),
([], "langgraph.checkpoint.base", "CheckpointMetadata", "checkpoints"),
([], "langgraph.checkpoint.base", "BaseCheckpointSaver", "checkpoints"),
([], "langgraph.checkpoint.base", "SerializerProtocol", "checkpoints"),
([], "langgraph.checkpoint.serde.jsonplus", "JsonPlusSerializer", "checkpoints"),
([], "langgraph.checkpoint.memory", "MemorySaver", "checkpoints"),
([], "langgraph.checkpoint.memory", "InMemorySaver", "checkpoints"),
([], "langgraph.checkpoint.sqlite.aio", "AsyncSqliteSaver", "checkpoints"),
([], "langgraph.checkpoint.sqlite", "SqliteSaver", "checkpoints"),
([], "langgraph.checkpoint.postgres.aio", "AsyncPostgresSaver", "checkpoints"),
([], "langgraph.checkpoint.postgres", "PostgresSaver", "checkpoints"),
# other prebuilts
(["langgraph_supervisor"], "langgraph_supervisor.supervisor", "create_supervisor", "supervisor"),
(["langgraph_supervisor"], "langgraph_supervisor.handoff", "create_handoff_tool", "supervisor"),
([], "langgraph_supervisor.handoff", "create_forward_message_tool", "supervisor"),
(["langgraph_swarm"], "langgraph_swarm.swarm", "create_swarm", "swarm"),
(["langgraph_swarm"], "langgraph_swarm.swarm", "add_active_agent_router", "swarm"),
(["langgraph_swarm"], "langgraph_swarm.swarm", "SwarmState", "swarm"),
(["langgraph_swarm"], "langgraph_swarm.handoff", "create_handoff_tool", "swarm"),
([], "langchain_mcp_adapters.client", "MultiServerMCPClient", "mcp"),
([], "langchain_mcp_adapters.tools", "load_mcp_tools", "mcp"),
([], "langchain_mcp_adapters.prompts", "load_mcp_prompt", "mcp"),
([], "langchain_mcp_adapters.resources", "load_mcp_resources", "mcp"),
]
WELL_KNOWN_LANGGRAPH_OBJECTS = {
@@ -90,154 +71,227 @@ WELL_KNOWN_LANGGRAPH_OBJECTS = {
}
def _make_regular_expression(pkg_prefix: str) -> re.Pattern:
if not pkg_prefix.isidentifier():
raise ValueError(f"Invalid package prefix: {pkg_prefix}")
return re.compile(
r"from\s+(" + pkg_prefix + r"(?:_\w+)?(?:\.\w+)*?)\s+import\s+\(?"
r"((?:\w+(?:,\s*)?)*)\s*\)?", # Match zero or more words separated by a comma+optional ws
re.DOTALL, # Match newlines as well
)
# Regular expression to match langchain import lines
_IMPORT_LANGCHAIN_RE = _make_regular_expression("langchain")
_IMPORT_LANGGRAPH_RE = _make_regular_expression("langgraph")
@lru_cache(maxsize=10_000)
def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
def _get_full_module_name(
module_path: str, class_name: str | None, doc_title: str
) -> Optional[str]:
"""Get full module name using inspect, with LRU cache to memoize results."""
try:
module = importlib.import_module(module_path)
symbol = getattr(module, class_name)
# First check the __module__ attribute on the symbol.
mod_name = getattr(symbol, "__module__", None)
# If __module__ is not set or comes from typing,
# assume the definition is in module_path.
if mod_name is None or mod_name.startswith("typing"):
return module_path
return mod_name
if module_path in sys.modules:
module = sys.modules[module_path]
else:
spec: ModuleSpec | None = importlib.util.find_spec(module_path)
if spec is not None:
module = importlib.util.module_from_spec(spec)
sys.modules[module_path] = module
spec.loader.exec_module(module)
if class_name is not None:
class_ = getattr(module, class_name)
if re.match(r"\w+\s+as\s+\w+", class_name):
# Handle cases like "A as B"
class_name, _ = class_name.split(" as ")
module = inspect.getmodule(class_)
if module is None:
# For constants, inspect.getmodule() might return None
# In this case, we'll return the original module_path
return module_path
return module.__name__
except AttributeError as e:
logger.warning(f"API Reference: Could not find module for {class_name}, {e}")
if class_name is not None:
# the class_name might actually be a module
# e.g. from langchain import hub
# try to import it as a module, and if that doesn't work, throw
if class_name is not None:
module_name = _get_full_module_name(
f"{module_path}.{class_name}", None, doc_title
)
if module_name is not None:
# return the name of the parent module, rather than the name of the class as though it were a module
return module.__name__
logger.warning(
f"API Reference: Could not find module for {class_name} in {module_path}, imported in doc {doc_title}, {e}"
)
# don't log if we're trying to import the "hub" part as though it were a module
logger.warning(
f"API Reference: Could not find module for {module_path}, imported in doc {doc_title}, {e}"
)
return None
except ImportError as e:
logger.warning(f"API Reference: Failed to load for class {class_name}, {e}")
logger.warning(
f"API Reference: Failed to import module {module_path} {doc_title}, {e}"
)
return None
def _get_doc_title(data: str, file_name: str) -> str:
try:
return re.findall(r"^#\s*(.*)", data, re.MULTILINE)[0]
except IndexError:
pass
# Parse the rst-style titles
try:
return re.findall(r"^(.*)\n=+\n", data, re.MULTILINE)[0]
except IndexError:
return file_name
class ImportInformation(TypedDict):
imported: str # The name of the class that was imported.
source: str # The full module path from which the class was imported.
docs: str # The URL pointing to the class's documentation.
path: str # The path of the file where the markdown content originated.
title: str # The title of the document where the import is used.
def get_imports(code: str, path: str) -> List[ImportInformation]:
"""Retrieve all import references from the given code for specified ecosystems.
def _get_imports(
code: str, doc_title: str, package_ecosystem: Literal["langchain", "langgraph"]
) -> List[ImportInformation]:
"""Get imports from the given code block.
Args:
code: The source code from which to extract import references.
path: The path of the file where the markdown content originated.
code: Python code block from which to extract imports
doc_title: Title of the document
package_ecosystem: "langchain" or "langgraph". The two live in different
repositories and have separate documentation sites.
Returns:
A list of import information for each import found.
List of import information for the given code block
"""
# Parse the code into an AST.
try:
tree = ast.parse(code)
except SyntaxError:
return []
imports = []
found_imports = []
if package_ecosystem == "langchain":
pattern = _IMPORT_LANGCHAIN_RE
elif package_ecosystem == "langgraph":
pattern = _IMPORT_LANGGRAPH_RE
else:
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
# Walk through the AST and process ImportFrom nodes.
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom):
# node.module is the source module.
if node.module is None:
for import_match in pattern.finditer(code):
module = import_match.group(1)
if "pydantic_v1" in module:
continue
imports_str = (
import_match.group(2).replace("(\n", "").replace("\n)", "")
) # Handle newlines within parentheses
# remove any newline and spaces, then split by comma
imported_classes = [
imp.strip()
for imp in re.split(r",\s*", imports_str.replace("\n", ""))
if imp.strip()
]
for class_name in imported_classes:
if module == "langchain_core.messages" and class_name == ")":
print("WARNING: ", file=sys.stderr)
print(
f"WARNING: Trying to import {class_name} from {module} in doc {doc_title}",
file=sys.stderr,
)
print("WARNING: ", file=sys.stderr)
print("WARNING:", import_match.group(0), file=sys.stderr)
print("WARNING: ", file=sys.stderr)
print(
"\n".join([f"WARNING: {line}" for line in code.splitlines()]),
file=sys.stderr,
)
print("WARNING: ", file=sys.stderr)
module_path = _get_full_module_name(module, class_name, doc_title)
if not module_path:
continue
for alias in node.names:
if not (
node.module.startswith("langchain")
or node.module.startswith("langgraph")
):
if len(module_path.split(".")) < 2:
continue
if package_ecosystem == "langchain":
pkg = module_path.split(".")[0].replace("langchain_", "")
top_level_mod = module_path.split(".")[1]
url = (
_LANGCHAIN_API_REFERENCE
+ pkg
+ "/"
+ top_level_mod
+ "/"
+ module_path
+ "."
+ class_name
+ ".html"
)
elif package_ecosystem == "langgraph":
if (module, class_name) not in WELL_KNOWN_LANGGRAPH_OBJECTS:
# Likely not documented yet
continue
found_imports.append(
{
"source": node.module,
# alias.name is the original name even if an alias exists.
"imported": alias.name,
}
source_module, namespace = WELL_KNOWN_LANGGRAPH_OBJECTS[
(module, class_name)
]
url = (
_LANGGRAPH_API_REFERENCE
+ namespace
+ "/#"
+ source_module
+ "."
+ class_name
)
else:
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
imports: list[ImportInformation] = []
for found_import in found_imports:
module = found_import["source"]
if module.startswith("langchain_mcp_adapters"):
package_ecosystem = "langgraph"
elif module.startswith("langchain"):
# Handles things like `langchain` or `langchain_anthropic`
package_ecosystem = "langchain"
elif module.startswith("langgraph"):
package_ecosystem = "langgraph"
else:
continue
class_name = found_import["imported"]
module_path = _get_full_module_name(module, class_name)
if not module_path:
continue
if len(module_path.split(".")) < 2:
continue
if package_ecosystem == "langchain":
pkg = module_path.split(".")[0].replace("langchain_", "")
top_level_mod = module_path.split(".")[1]
url = (
_LANGCHAIN_API_REFERENCE
+ pkg
+ "/"
+ top_level_mod
+ "/"
+ module_path
+ "."
+ class_name
+ ".html"
# Add the import information to our list
imports.append(
{
"imported": class_name,
"source": module,
"docs": url,
"title": doc_title,
}
)
elif package_ecosystem == "langgraph":
if (module, class_name) not in WELL_KNOWN_LANGGRAPH_OBJECTS:
# Likely not documented yet
continue
source_module, namespace = WELL_KNOWN_LANGGRAPH_OBJECTS[
(module, class_name)
]
url = (
_LANGGRAPH_API_REFERENCE
+ namespace
+ "/#"
+ source_module
+ "."
+ class_name
)
else:
raise ValueError(f"Invalid package ecosystem: {package_ecosystem}")
# Add the import information to our list
imports.append(
{
"imported": class_name,
"source": module,
"docs": url,
"path": path,
}
)
return imports
def update_markdown_with_imports(markdown: str, path: str) -> str:
def get_imports(code: str, doc_title: str) -> List[ImportInformation]:
"""Retrieve all import references from the given code for specified ecosystems.
Args:
code: The source code from which to extract import references.
doc_title: The documentation title associated with the code.
Returns:
A list of import information for each import found.
"""
ecosystems = ["langchain", "langgraph"]
all_imports = []
for package_ecosystem in ecosystems:
all_imports.extend(_get_imports(code, doc_title, package_ecosystem))
return all_imports
def update_markdown_with_imports(markdown: str, file_name: str) -> str:
"""Update markdown to include API reference links for imports in Python code blocks.
This function scans the markdown content for Python code blocks, extracts any
imports, and appends links to their API documentation.
This function scans the markdown content for Python code blocks, extracts any imports, and appends links to their API documentation.
Args:
markdown: The markdown content to process.
path: The path of the file where the markdown content originated.
Returns:
Updated markdown with API reference links prepended to Python code blocks.
Updated markdown with API reference links appended to Python code blocks.
Example:
Given a markdown with a Python code block:
@@ -245,8 +299,7 @@ def update_markdown_with_imports(markdown: str, path: str) -> str:
```python
from langchain.nlp import TextGenerator
```
This function will append an API reference link to the `TextGenerator` class
from the `langchain.nlp` module if it's recognized.
This function will append an API reference link to the `TextGenerator` class from the `langchain.nlp` module if it's recognized.
"""
code_block_pattern = re.compile(
r"(?P<indent>[ \t]*)```(?P<language>python|py)\n(?P<code>.*?)\n(?P=indent)```",
@@ -260,12 +313,13 @@ def update_markdown_with_imports(markdown: str, path: str) -> str:
match (re.Match): The regex match object containing the code block.
Returns:
str: The modified code block with API reference links prepended if applicable.
str: The modified code block with API reference links appended if applicable.
"""
indent = match.group("indent")
code_block = match.group("code")
language = match.group("language") # Preserve the language from the regex match
# Retrieve import information from the code block
imports = get_imports(code_block, "__unused__")
imports = get_imports(code_block, file_name)
original_code_block = match.group(0)
# If no imports are found, return the original code block
@@ -276,8 +330,8 @@ def update_markdown_with_imports(markdown: str, path: str) -> str:
api_links = " | ".join(
f'<a href="{imp["docs"]}">{imp["imported"]}</a>' for imp in imports
)
# Return the code block with prepended API reference links
return f"{indent}<sup><i>API Reference: {api_links}</i></sup>\n\n{original_code_block}"
# Return the code block with appended API reference links
return f"{original_code_block}\n\n{indent}API Reference: {api_links}"
# Apply the replace_code_block function to all matches in the markdown
updated_markdown = code_block_pattern.sub(replace_code_block, markdown)
+4
View File
@@ -0,0 +1,4 @@
hook_state = {
"document_filename": "__UNKNOWN__",
"document_content": "__UNKNOWN__",
}
+123 -35
View File
@@ -1,7 +1,10 @@
import argparse
import ast
import glob
import os
import re
from typing import Literal
from pathlib import Path
from typing import Literal, Optional
import nbformat
from nbconvert.exporters import MarkdownExporter
@@ -25,7 +28,7 @@ def _uses_input(source: str) -> bool:
def _rewrite_cell_magic(code: str) -> str:
"""Process a code block that uses cell magic.
"""Process a code block that uses cell magic.:w
- Lines starting with "%%capture" are ignored.
- Lines starting with "%pip" are rewritten by removing the leading "%" character.
@@ -51,14 +54,10 @@ def _rewrite_cell_magic(code: str) -> str:
if stripped.startswith("%%capture"):
continue
# Rewrite %pip lines by dropping the '%'
elif stripped.startswith("%") or stripped.startswith("!"):
# Drop the leading '%' character and then drop all leading whitespace
stripped = stripped.lstrip("%! \t")
# Check if the line starts with "pip"
if stripped.startswith("pip"):
rewritten_lines.append(stripped)
else:
raise NotImplementedError(f"Unhandled line: {line}")
elif stripped.startswith("%pip"):
# Drop the leading '%' character
rewritten_lines.append(stripped[1:])
# Anything else is not supported
else:
raise NotImplementedError(f"Unhandled line: {line}")
@@ -220,24 +219,6 @@ def _convert_links_in_markdown(markdown: str) -> str:
)
class HideCellTagPreprocessor(Preprocessor):
"""
Removes cells that have '# hide-cell' at the beginning of the cell content.
This allows authors to include cells in the notebook that should not
appear in the generated markdown output.
"""
def preprocess(self, nb, resources):
# Filter out cells with the '# hide-cell' comment at the beginning
nb.cells = [
cell
for cell in nb.cells
if not (cell.source.strip().startswith("# hide-cell"))
]
return nb, resources
class EscapePreprocessor(Preprocessor):
def __init__(self, markdown_exec_migration: bool = False, **kwargs) -> None:
super().__init__(**kwargs)
@@ -268,10 +249,13 @@ class EscapePreprocessor(Preprocessor):
)
cell.metadata["exec"] = is_exec
# For markdown exec migration we'll re-write cell magic as bash commands
if source.startswith("%%"):
cell.source = _rewrite_cell_magic(source)
cell.metadata["language"] = "shell"
if self.markdown_exec_migration:
# For markdown exec migration we'll re-write cell magic as bash commands
if source.startswith("%%"):
cell.source = _rewrite_cell_magic(source)
cell.metadata["language"] = "shell"
cell.metadata["has_output"] = _has_output(source)
# Remove noqa comments
cell.source = re.sub(r"#\s*noqa.*$", "", cell.source, flags=re.MULTILINE)
@@ -359,7 +343,6 @@ class ExtractAttachmentsPreprocessor(Preprocessor):
exporter = MarkdownExporter(
preprocessors=[
HideCellTagPreprocessor,
EscapePreprocessor,
ExtractAttachmentsPreprocessor,
],
@@ -369,14 +352,119 @@ exporter = MarkdownExporter(
],
)
md_executable = MarkdownExporter(
preprocessors=[
ExtractAttachmentsPreprocessor,
EscapePreprocessor(markdown_exec_migration=True),
],
template_name="md_executable",
extra_template_basedirs=[
os.path.join(os.path.dirname(__file__), "notebook_convert_templates")
],
)
def convert_notebook(
notebook_path: str,
notebook_path: Path,
mode: Literal["markdown", "exec"] = "markdown",
) -> str:
with open(notebook_path) as f:
nb = nbformat.read(f, as_version=4)
nb.metadata.mode = mode
body, _ = exporter.from_notebook_node(nb)
if mode == "markdown":
body, _ = exporter.from_notebook_node(nb)
else:
body, _ = md_executable.from_notebook_node(nb)
return body
HERE = Path(__file__).parent
DOCS = HERE.parent / "docs"
# Convert notebooks to markdown
def _convert_notebooks(
*,
output_dir: Optional[Path] = None,
replace: bool = False,
pattern: str = "*.ipynb",
) -> None:
"""Converting notebooks."""
if not output_dir and not replace:
raise ValueError("Either --output_dir or --replace must be specified")
output_dir_path = DOCS if replace else Path(output_dir)
# Get the directory where the script was executed
base_dir = os.getcwd()
# Build the full search pattern using the current working directory as the base
full_pattern = os.path.join(base_dir, args.pattern)
# Use glob with recursive search enabled
matching_files = glob.glob(full_pattern, recursive=True)
paths = [Path(file) for file in matching_files]
file_names = [notebook.name for notebook in paths]
for notebook in paths:
markdown = convert_notebook(notebook, mode="exec")
markdown_path = output_dir_path / notebook.relative_to(DOCS).with_suffix(".md")
markdown_path.parent.mkdir(parents=True, exist_ok=True)
with open(markdown_path, "w") as f:
f.write(markdown)
if replace:
notebook.unlink(missing_ok=False)
if replace:
# The regex will match markdown links that point to *.ipynb files.
# It captures:
# group(1): the link text (inside the square brackets)
# group(2): the file path (without the trailing .ipynb)
link_pattern = r"(?<!!)\[([^\]]+)\]\((?![^)]*//)([^)]+)\.ipynb\)"
def replace_link(match: re.Match) -> str:
link_text = match.group(1)
link_target = match.group(2)
# Reconstruct the file name with the .ipynb extension.
# For example, if link_target is "foo/bar", then linked_file becomes "bar.ipynb".
linked_file = Path(link_target).name + ".ipynb"
# Only update if the notebook was among those converted.
if linked_file in file_names:
# Change the extension from .ipynb to .md
return f"[{link_text}]({link_target}.md)"
# Otherwise, leave the original link intact.
return match.group(0)
# Process all markdown files in the output directory.
for path in output_dir_path.rglob("**/*.md"):
with open(path, "r", encoding="utf-8") as f:
content = f.read()
new_content = re.sub(link_pattern, replace_link, content)
with open(path, "w", encoding="utf-8") as f:
f.write(new_content)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Convert notebooks to markdown")
parser.add_argument(
"--output_dir",
default=None,
help="Directory to output markdown files",
)
parser.add_argument(
"--replace",
action="store_true",
help="Replace original notebooks with markdown files",
)
parser.add_argument(
"--pattern",
default="*.ipynb",
help="Glob pattern to match notebooks to convert",
)
args = parser.parse_args()
_convert_notebooks(
replace=args.replace,
output_dir=args.output_dir,
pattern=args.pattern,
)
@@ -0,0 +1,5 @@
{
"mimetypes": {
"text/markdown": true
}
}
@@ -0,0 +1,38 @@
{#https://github.com/rdbisme/nbconvert/blob/master/share/jupyter/nbconvert/templates/markdown/index.md.j2#}
{% extends 'markdown/index.md.j2' %}
{% block input %}
```
{%- if 'magics_language' in cell.metadata -%}
{{ cell.metadata.magics_language}}
{%- elif cell.metadata.get('language') == "shell" -%}
shell
{%- elif 'name' in nb.metadata.get('language_info', {}) -%}
{{ nb.metadata.language_info.name }}{% if cell.metadata.exec|default(false) %} exec="on" source="above" session="1"{% if cell.metadata.has_output|default(false) %} result="ansi"{% endif %}{% endif %}
{%- endif %}
{{ cell.source}}
```
{% endblock input %}
{%- block traceback_line -%}
{%- endblock traceback_line -%}
{%- block stream -%}
{%- endblock stream -%}
{%- block data_text scoped -%}
{%- endblock data_text -%}
{%- block data_html scoped -%}
```html
{{ output.data['text/html'] | safe }}
```
{%- endblock data_html -%}
{%- block data_jpg scoped -%}
![](data:image/jpg;base64,{{ output.data['image/jpeg'] }})
{%- endblock data_jpg -%}
{%- block data_png scoped -%}
![](data:image/png;base64,{{ output.data['image/png'] }})
{%- endblock data_png -%}
@@ -1,18 +1,5 @@
{% extends 'markdown/index.md.j2' %}
{% block input %}{# cell.metadata.language is an addition of our docs pipeline. #}
```{%- if 'language' in cell.metadata -%}
{{ cell.metadata.language }}
{%- elif 'magics_language' in cell.metadata -%}
{{ cell.metadata.magics_language }}
{%- elif 'name' in nb.metadata.get('language_info', {}) -%}
{{ nb.metadata.language_info.name }}
{%- endif %}
{{ cell.source }}
```
{% endblock input %}
{%- block traceback_line -%}
```output
{{ line.rstrip() | strip_ansi }}
@@ -21,13 +8,13 @@
{%- block stream -%}
```output
{{ output.text.rstrip() | strip_ansi }}
{{ output.text.rstrip() }}
```
{%- endblock stream -%}
{%- block data_text scoped -%}
```output
{{ output.data['text/plain'].rstrip() | strip_ansi }}
{{ output.data['text/plain'].rstrip() }}
```
{%- endblock data_text -%}
+123 -40
View File
@@ -2,13 +2,21 @@ import logging
import os
import posixpath
import re
from typing import Any, Dict
import traceback
from typing import Any, Callable, Dict
from markdown import Markdown
from mkdocs.structure.files import Files, File
from mkdocs.structure.pages import Page
from pymdownx.superfences import SuperFencesException
from _scripts.hook_state import hook_state
from markdown_exec.hooks import SessionHistoryEntry
from _scripts.generate_api_reference_links import update_markdown_with_imports
from _scripts.notebook_convert import convert_notebook
from _scripts.setup_vcr import get_hash_for_session, load_postamble, load_preamble, _hash_string
logger = logging.getLogger(__name__)
logging.basicConfig()
@@ -31,15 +39,6 @@ REDIRECT_MAP = {
"cloud/concepts/api.md": "concepts/langgraph_server.md",
"cloud/concepts/cloud.md": "concepts/langgraph_cloud.md",
"cloud/faq/studio.md": "concepts/langgraph_studio.md#studio-faqs",
# prebuit redirects
"how-tos/create-react-agent.ipynb": "agents/agents.md#basic-configuration",
"how-tos/create-react-agent-memory.ipynb": "agents/memory.md",
"how-tos/create-react-agent-system-prompt.ipynb": "agents/context.md#prompts",
"how-tos/create-react-agent-hitl.ipynb": "agents/human-in-the-loop.md",
"how-tos/create-react-agent-structured-output.ipynb": "agents/agents.md#structured-output",
# misc
"prebuilt.md": "agents/prebuilt.md",
"reference/prebuilt.md": "reference/agents.md"
}
@@ -66,29 +65,6 @@ def on_files(files: Files, **kwargs: Dict[str, Any]):
return new_files
def _add_path_to_code_blocks(markdown: str, page: Page) -> str:
"""Add the path to the code blocks."""
code_block_pattern = re.compile(
r"(?P<indent>[ \t]*)```(?P<language>\w+)[ ]*(?P<attributes>[^\n]*)\n"
r"(?P<code>((?:.*\n)*?))" # Capture the code inside the block using named group
r"(?P=indent)```" # Match closing backticks with the same indentation
)
def replace_code_block_header(match: re.Match) -> str:
indent = match.group("indent")
language = match.group("language")
attributes = match.group("attributes").rstrip()
if 'exec="on"' not in attributes:
# Return original code block
return match.group(0)
code = match.group("code")
return f'{indent}```{language} {attributes} path="{page.file.src_path}"\n{code}{indent}```'
return code_block_pattern.sub(replace_code_block_header, markdown)
def _highlight_code_blocks(markdown: str) -> str:
"""Find code blocks with highlight comments and add hl_lines attribute.
@@ -166,6 +142,110 @@ def _highlight_code_blocks(markdown: str) -> str:
markdown = code_block_pattern.sub(replace_highlight_comments, markdown)
return markdown
def handle_vcr_setup(
*,
formatter: Callable,
language: str,
code: str,
session: str,
id: str,
md: Markdown,
**kwargs: Dict[str, Any],
) -> Dict[str, Any]:
"""Handle VCR setup in markdown content if necessary."""
logger.info(f"handle_vcr_setup: {hook_state['document_filename']}")
try:
if hook_state['document_filename'] == '__UNKNOWN__':
raise SuperFencesException(
f"error while processing {language} block: document filename hasn't been set yet"
)
if hook_state['document_content'] == '__UNKNOWN__':
raise SuperFencesException(
f"error while processing {language} block: document content hasn't been set yet"
)
if session is None or session == "" and id is None or id == "":
id = _hash_string(code)
if session is not None and session != "":
logger.info(f"new {language} session {session} on page {hook_state['document_filename']}")
cassette_prefix = hook_state['document_filename'].replace(".md", "").replace(os.path.sep, "_")
cassette_dir = os.path.abspath(
os.path.join(os.path.dirname(os.path.dirname(__file__)), "cassettes")
)
os.makedirs(cassette_dir, exist_ok=True)
# Build a unique cassette name.
cassette_name = os.path.join(
cassette_dir,
f"{cassette_prefix}_{session if session else id}_{language}.msgpack.zlib",
)
# Add context manager at start with explicit __enter__ and __exit__ calls
hash_ = get_hash_for_session(language, session, hook_state['document_content'])
wrapped_lines = [
load_preamble(language, hash_, cassette_name),
code,
]
if session is None or session == "":
logger.info(
f"no session, adding postamble for {language} in {hook_state['document_filename']}"
)
wrapped_lines.append(load_postamble(language))
transformed_source = "\n".join(wrapped_lines)
# Propagate extras
keep_extras = {
key: value
for key, value in kwargs["extra"].items()
if key
in {
"hl_lines",
}
}
return dict(
transform_source=lambda code: (transformed_source, code),
id=id,
extra={ **keep_extras, "path": hook_state['document_filename'] },
)
except Exception as e:
raise SuperFencesException(traceback.format_exc()) from e
def handle_vcr_teardown(
*,
formatter: Callable,
language: str,
session: str,
history: list[SessionHistoryEntry],
):
code = load_postamble(language)
html = False
update_toc = False
logger.info(f"tearing down {language} {session} on {hook_state['document_filename']}")
kwargs = dict(
code=code,
session=session,
id=f"{id}_vcr_end",
md=None, # md is unused by the formatter, but it's a required argument
html=html,
update_toc=update_toc,
extra={},
)
# This doesn't actually render anything, we just call the formatter so it
# executes in the same context as the session of which we're disposing.
formatter(**kwargs)
def _on_page_markdown_with_config(
markdown: str,
@@ -184,23 +264,20 @@ def _on_page_markdown_with_config(
# Append API reference links to code blocks
if add_api_references:
markdown = update_markdown_with_imports(markdown, page.file.abs_src_path)
markdown = update_markdown_with_imports(markdown, page.file.src_path)
# Apply highlight comments to code blocks
markdown = _highlight_code_blocks(markdown)
# Add file path as an attribute to code blocks that are executable.
# This file path is used to associate fixtures with the executable code
# which can be used in CI to test the docs without making network requests.
markdown = _add_path_to_code_blocks(markdown, page)
if remove_base64_images:
# Remove base64 encoded images from markdown
markdown = re.sub(r"!\[.*?\]\(data:image/[^;]+;base64,[^)]+\)", "", markdown)
markdown = re.sub(r"!\[.*?\]\(data:image/+;base64,[^\)]+\)", "", markdown)
return markdown
def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
logger.info(f"on_page_markdown: {page.file.src_path}")
hook_state['document_filename'] = page.file.src_path
return _on_page_markdown_with_config(
markdown,
page,
@@ -262,3 +339,9 @@ def on_post_build(config):
+ suffix
)
write_html(config["site_dir"], old_html_path, new_html_path)
def on_pre_page(page: Page, **kwargs: Dict[str, Any]):
logger.info(f"on_pre_page: {page.file.src_path}")
hook_state['document_filename'] = page.file.src_path
hook_state['document_content'] = page.file.content_string
return page
+2 -22
View File
@@ -20,6 +20,7 @@ BLOCKLIST_COMMANDS = (
NOTEBOOKS_NO_CASSETTES = (
"docs/how-tos/visualization.ipynb",
"docs/how-tos/many-tools.ipynb"
)
NOTEBOOKS_NO_EXECUTION = [
@@ -48,10 +49,7 @@ NOTEBOOKS_NO_EXECUTION = [
"docs/how-tos/map-reduce.ipynb", # flakiness from structured output, only when running with VCR
"docs/tutorials/tot/tot.ipynb",
"docs/how-tos/visualization.ipynb",
"docs/how-tos/streaming-specific-nodes.ipynb",
"docs/tutorials/llm-compiler/LLMCompiler.ipynb",
"docs/tutorials/customer-support/customer-support.ipynb", # relies on openai embeddings, doesn't play well w/ VCR
"docs/how-tos/many-tools.ipynb", # relies on openai embeddings, doesn't play well w/ VCR
"docs/tutorials/llm-compiler/LLMCompiler.ipynb"
]
@@ -88,13 +86,6 @@ def has_blocklisted_command(code: str, metadata: dict) -> bool:
return True
return False
def remove_mermaid(code: str) -> str:
return code.replace(
"display(Image(graph.get_graph().draw_mermaid_png()))",
# replace with a dummy statement
"print()"
)
def add_vcr_to_notebook(
notebook: nbformat.NotebookNode, cassette_prefix: str
@@ -189,15 +180,6 @@ def add_vcr_to_notebook(
return notebook
def remove_mermaid_from_notebook(notebook: nbformat.NotebookNode) -> nbformat.NotebookNode:
for cell in notebook.cells:
if cell.cell_type != "code":
continue
cell.source = remove_mermaid(cell.source)
return notebook
def process_notebooks(should_comment_install_cells: bool) -> None:
for directory in NOTEBOOK_DIRS:
for root, _, files in os.walk(directory):
@@ -219,8 +201,6 @@ def process_notebooks(should_comment_install_cells: bool) -> None:
notebook, cassette_prefix=cassette_prefix
)
notebook = remove_mermaid_from_notebook(notebook)
if notebook_path in NOTEBOOKS_NO_EXECUTION:
# Add a cell at the beginning to indicate that this notebook should not be executed
warning_cell = nbformat.v4.new_markdown_cell(
+140
View File
@@ -0,0 +1,140 @@
# A list of patterns that, if found in a code block, will cause us to leave that block unchanged.
import hashlib
import json
import os
import re
from textwrap import dedent, indent
from mistune import BlockParser, BlockState, Markdown, create_markdown
from mistune.renderers.markdown import MarkdownRenderer
preambles = {
"python": "vcr_setup_preamble.py",
"typescript": "nock_setup_preamble.ts",
}
def _get_python_cassette_init(cassette_name: str, hash_: str) -> str:
return dedent(
f"""
_cassette = HashedCassette('{cassette_name}', '{hash_}')
_cassette.__enter__()
"""
)
def _get_typescript_cassette_init(cassette_name: str, hash_: str) -> str:
return dedent(
f"""
const _cassette = new HashedCassette("{cassette_name}", "{hash_}");
await _cassette.enter();
"""
)
def _get_python_cassette_cleanup() -> str:
return "_cassette.__exit__()"
def _get_typescript_cassette_cleanup() -> str:
return "await _cassette.exit();"
preamble_inits = {
"python": _get_python_cassette_init,
"py": _get_python_cassette_init,
"typescript": _get_typescript_cassette_init,
"ts": _get_typescript_cassette_init,
}
preamble_cleanups = {
"python": _get_python_cassette_cleanup,
"py": _get_python_cassette_cleanup,
"typescript": _get_typescript_cassette_cleanup,
"ts": _get_typescript_cassette_cleanup,
}
def load_preamble(language: str, hash_: str, cassette_name: str) -> str:
"""Load the source code for the preamble for a given language."""
_assets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
preamble_path = os.path.join(_assets_dir, preambles[language])
with open(preamble_path, "r") as f:
lines = f.readlines()
lines.append(preamble_inits[language](cassette_name, hash_))
return "\n".join(lines).strip()
def load_postamble(language: str) -> str:
"""Load the source code for the postamble for a given language."""
return preamble_cleanups[language]()
def _hash_string(input_string: str) -> str:
# Encode the input string to bytes
encoded_string = input_string.encode("utf-8")
# Create a SHA-256 hash object
sha256_hash = hashlib.sha256(encoded_string)
# Get the hexadecimal digest of the hash
return sha256_hash.hexdigest()
def extract_code_blocks_for_session(language: str, session: str, content: str) -> str:
code_blocks_for_session = []
TAB_REGEX = r"^===!? \"(?P<title>[^\"]+)\"\n(?P<content>(?:(?P<indent> )+[^\n]*\n)+)"
def parse_tabs(block: BlockParser, m: re.Match, state: BlockState) -> str:
state.append_token(
{
"raw": m.group(0),
"type": "block_tab",
"attrs": {
"title": m.group("title"),
"level": len(m.group("indent")) // 4,
"content": dedent(m.group("content")).strip(),
},
}
)
return m.end()
def render_tabs(self, token: dict, state: BlockState):
recursive_transformer = create_markdown(renderer=DocumentRenderer())
recursive_transformer.block.register("block_tab", TAB_REGEX, parse_tabs, before='list')
recursive_transformer.renderer.register("block_tab", render_tabs)
return (
f'=== "{token["attrs"]["title"]}"\n'
f'{indent(recursive_transformer(token["attrs"]["content"]), " " * token["attrs"]["level"])}\n'
)
class DocumentRenderer(MarkdownRenderer):
def block_code(self, token: dict, state: BlockState):
if token["style"] == "fenced":
if token["attrs"]["info"]:
attributes = {}
block_language = token["attrs"]["info"].split()[0]
for match in re.finditer(r'(?P<key>\w+)=(?:(?P<value>(?:[\w]+))|"(?P<value_quoted>(?:[^"\s]+))")', token["attrs"]["info"]):
attributes[match.group("key")] = match.group("value") or match.group("value_quoted")
if block_language == language and "session" in attributes and attributes["session"] == session:
code_blocks_for_session.append(token["raw"].rstrip())
return super().block_code(token, state)
transformer: Markdown = create_markdown(renderer=DocumentRenderer())
transformer.block.register("block_tab", TAB_REGEX, parse_tabs, before='list')
transformer.renderer.register("block_tab", render_tabs)
# Parses the page content, which causes the code blocks to be added to the code_blocks_for_session list.
# There's probably some way to do this by using the renderer as a filter, but I would've had to NO-OP
# all of the default behavior, and this was easier.
transformer(content)
return code_blocks_for_session
def get_hash_for_session(language: str, session: str, content: str) -> str:
# include the preamble in the hash so we invalidate if it changes
preamble_hash = _hash_string(load_preamble(language, session, "test"))
code_blocks_for_session = [preamble_hash, *extract_code_blocks_for_session(language, session, content)]
return _hash_string("\n".join(code_blocks_for_session))
@@ -9,7 +9,10 @@ import yaml
MARKDOWN = """\
[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!)
# Community Agents
# 🚀 Prebuilt Agents
LangGraph includes a prebuilt React agent. For more information on how to use it,
check out our [how-to guides](https://langchain-ai.github.io/langgraph/how-tos/#prebuilt-react-agent).
If you’re looking for other prebuilt libraries, explore the community-built options
below. These libraries can extend LangGraph's functionality in various ways.
@@ -80,18 +83,14 @@ def generate_markdown(resolved_packages: List[ResolvedPackage], language: str) -
resolved_packages, key=lambda p: p["weekly_downloads"] or 0, reverse=True
)
rows = [
"| Name | GitHub URL | Description | Weekly Downloads | Stars |",
"| --- | --- | --- | --- | --- |",
"| Name | GitHub URL | Description | Weekly Downloads |",
"| --- | --- | --- | --- |",
]
for package in sorted_packages:
name = f"**{package['name']}**"
repo_url = f"[{package['repo']}](https://github.com/{package['repo']})"
stars_badge = (
f"https://img.shields.io/github/stars/{package['repo']}?style=social"
)
stars = f"![GitHub stars]({stars_badge})"
downloads = package["weekly_downloads"] or "-"
row = f"| {name} | {repo_url} | {package['description']} | {downloads} | {stars}"
downloads = package["weekly_downloads"] or 0
row = f"| {name} | {repo_url} | {package['description']} | {downloads} |"
rows.append(row)
markdown_content = MARKDOWN.format(
library_list="\n".join(rows), langgraph_url=langgraph_url
@@ -30,63 +30,25 @@ PACKAGES_FILE = HERE / "packages.yml"
PACKAGES = yaml.safe_load(PACKAGES_FILE.read_text())['packages']
def _get_weekly_downloads(packages: list[Package], fake: bool) -> list[ResolvedPackage]:
def _get_weekly_downloads(packages: list[Package]) -> list[ResolvedPackage]:
"""Retrieve the monthly download count for a list of packages from PyPIStats."""
resolved_packages: list[ResolvedPackage] = []
if fake:
# To avoid making network requests during testing, return fake download counts
for package in packages:
resolved_packages.append(
{
"name": package["name"],
"repo": package["repo"],
"weekly_downloads": -12345,
"description": package["description"],
}
)
return resolved_packages
for package in packages:
# First check if package exists on PyPI
pypi_url = f"https://pypi.org/pypi/{package['name']}/json"
try:
pypi_response = requests.get(pypi_url)
pypi_response.raise_for_status()
except requests.exceptions.HTTPError:
raise AssertionError(f"Package {package['name']} does not exist on PyPI")
url = f"https://pypistats.org/api/packages/{package['name']}/overall"
# Get first release date
pypi_data = pypi_response.json()
releases = pypi_data["releases"]
first_release_date = None
for version_releases in releases.values():
if version_releases: # Some versions may be empty lists
upload_time = datetime.fromisoformat(version_releases[0]["upload_time"])
if first_release_date is None or upload_time < first_release_date:
first_release_date = upload_time
response = requests.get(url)
response.raise_for_status()
data = response.json()
if first_release_date is None:
raise AssertionError(f"Package {package['name']} has no releases yet")
sorted_data = sorted(
data["data"],
key=lambda x: datetime.strptime(x["date"], "%Y-%m-%d"),
reverse=True,
)
# If package was published in last 48 hours, skip download stats
if (datetime.now() - first_release_date).total_seconds() >= 48 * 3600:
url = f"https://pypistats.org/api/packages/{package['name']}/overall"
response = requests.get(url)
response.raise_for_status()
data = response.json()
sorted_data = sorted(
data["data"],
key=lambda x: datetime.strptime(x["date"], "%Y-%m-%d"),
reverse=True,
)
# Sum the last 7 days of downloads
num_downloads = sum(entry["downloads"] for entry in sorted_data[:7])
else:
num_downloads = None
# Sum the last 7 days of downloads
num_downloads = sum(entry["downloads"] for entry in sorted_data[:7])
resolved_packages.append(
{
@@ -101,13 +63,13 @@ def _get_weekly_downloads(packages: list[Package], fake: bool) -> list[ResolvedP
def main(output_file: str, fake: bool) -> None:
def main(output_file: str) -> None:
"""Main function to generate package download information.
Args:
output_file: Path to the output YAML file.
"""
resolved_packages: list[ResolvedPackage] = _get_weekly_downloads(PACKAGES, fake)
resolved_packages: list[ResolvedPackage] = _get_weekly_downloads(PACKAGES)
if not output_file.endswith(".yml"):
raise ValueError("Output file must have a .yml extension")
@@ -128,15 +90,6 @@ if __name__ == "__main__":
"downloads.yml"
),
)
parser.add_argument(
"--fake",
default=False,
action="store_true",
help=(
"Generate fake download counts for testing purposes. "
"This option will not make any network requests."
),
)
args = parser.parse_args()
main(args.output_file, args.fake)
main(args.output_file)
+4 -34
View File
@@ -2,40 +2,10 @@
packages:
- name: "trustcall"
repo: "hinthornw/trustcall"
description: "Tenacious tool calling built on LangGraph."
description: "Tenacious tool calling built on LangGraph"
- name: "breeze-agent"
repo: "andrestorres123/breeze-agent"
description: "A streamlined research system built inspired on STORM and built on LangGraph."
description: "A streamlined research system built inspired on STORM and built on LangGraph"
- name: "langgraph-supervisor"
repo: "langchain-ai/langgraph-supervisor-py"
description: "Build supervisor multi-agent systems with LangGraph."
- name: "langmem"
repo: "langchain-ai/langmem"
description: "Build agents that learn and adapt from interactions over time."
- name: "langchain-mcp-adapters"
repo: "langchain-ai/langchain-mcp-adapters"
description: "Make Anthropic Model Context Protocol (MCP) tools compatible with LangGraph agents."
- name: "open-deep-research"
repo: "langchain-ai/open_deep_research"
description: "Open source assistant for iterative web research and report writing."
- name: "langgraph-swarm"
repo: "langchain-ai/langgraph-swarm-py"
description: "Build swarm-style multi-agent systems using LangGraph."
- name: "delve-taxonomy-generator"
repo: "andrestorres123/delve"
description: "A taxonomy generator for unstructured data"
- name: "nodeology"
repo: "xyin-anl/Nodeology"
description: "Enable researcher to build scientific workflows easily with simplified interface."
- name: "langgraph-bigtool"
repo: "langchain-ai/langgraph-bigtool"
description: "Build LangGraph agents with large numbers of tools."
- name: "ai-data-science-team"
repo: "business-science/ai-data-science-team"
description: "An AI-powered data science team of agents to help you perform common data science tasks 10X faster."
- name: "langgraph-reflection"
repo: "langchain-ai/langgraph-reflection"
description: "LangGraph agent that runs a reflection step."
- name: "langgraph-codeact"
repo: "langchain-ai/langgraph-codeact"
description: "LangGraph implementation of CodeAct agent that generates and executes code instead of tool calling."
repo: "langchain-ai/langgraph-supervisor"
description: "Build supervisor multi-agent systems with LangGraph"
@@ -1 +0,0 @@
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -1 +0,0 @@
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
eNptVX1wG0cVtxNm2k75I5Q0BYaPQxT+SH3SSSfrKzGpkD9qO65tyYntmFSz2ltJJ9/dXu72ZMnBZeLCTKEZ0mshbT46E2JZMq4T2xOTmCYuhJlCKC0ktMxgk5jpBw1N0uIATek0TNg7y6k9yc5Id7vvt7/33r7fvhssZZGmi1ipHBMVgjQACZ3o5mBJQzsMpJPvFWVE0lgotLXGOoYMTZxdnyZE1UMuF1BFJ1aRAkQnxLIr63bBNCAu+q5KyKYpJLCQn6s0djpkpOsghXRHiOnZ6YCY+lIInTj60phJGKJEmDw2NjmqGIeGJWRZDB1pjoHtdEXGApKspZRKWC+2QAqduulTJxoCMp0kgaQjukCQrNI0iKFZJJyTs9YwlsqeSV61yZOGYmdqcd18DzE7HQqQbUAKkXgfAiRNg6AYAelQE9UyzLFFRwxJizpDMEORTBnJiEoSazKwYE5rmwo0ykfPVbfJVY2el0ZEtDiFIsnbL0gxrBx6HEoeWtv0pMPKeylYmqSopBwDA9bp0LKIGhJsuE2wHIkTGQQJRQ5sHyilERCo5/mKNYU01ol5dGXBxgGEiB4oUiAWKL95JNUvqlWMgJISIGiUFklB9sGYo70IqSyQxCwqLu4yJ4CqSiK0U3VldKyMlYvKWrHcah61yslSCSjEnGqlQYQbXW15qiyFcTu9ASc3kWN1AkRFokphJUDjKaq2/eRygwpgLyVhy6o1i4ubjy7HYN0cbgGwNbaCEmgwbQ4DTfZ5jy1f1wyFiDIyS5G2W92VjZ+4451utzM4uYJYzyvQHLYFeGLFZkS0PAsx5TB/yhUhxr0iMmf/FY/DZDwh13gfatwa9ZI6Up3fKnbuaA9wwYa+Di3ToLdznQnJ2NpVz4ej6c3ddVtYt9/j56v9Ab6adTs5p9vpZsXIjs5wXT1NP1cfTjws8UEf9sUAJ2xriqbzXd3ZeHOz0KT2eTQYxFxtWPNG+FbEtzqbW2ItbVsVdUujFySi8d54Y6Chw+d01macXakNDI3OyIpCTa7Pi9S8Kqj+FqEr15/JkkAT2dwBxBS/rS6p5XbIqUatrbd+20NwWXgejme5coQ+zhvgrHF0SRsSUlIkbQ7xfn5EQ7pKewR6rEiPjBj6YIHqEL1yplTuFYdbmz+R8L2FWqpJc6YjbVQxHh8TQyrj4Txexs2HeH/IHWQaWjrGImU3HbeV4GSHBhQ9SWVYtyT5EkwbSi8SRiO3FfuMJXZaSSt82pZYlFOxjthyVOZYFxtd7JJsY+2xxZvFYi0FFLHfdmv+zBIy7YqiMlU20xZgUVLnrKybQ36f52jZsqSxUZoXx7o5lnP/wrr5kF4pK3AVa4TVEaQ9mOTN2SoZ5Kz7VMO7q3kfPeQNtPtAyRBQzEjUYpn61DcwqoYkDIQXciztiUgSZZEWwf4v93d6V9xWiaZvRRDci+inYITnFseLyyEasjxYadwkKgTpOHV70BKXx8IE/cEXVsJ0tCygIZ+sT99qL1Mc5vSx3BKYFQVz9n46ifsCXp6DMCj4gjznATDAAY+n2hOsdnuroRdx45F6NgJgGrExW21mqbb74XBLY+R4F7tcNmyruvgFLClYV8RkshhDGi2NOQolbAi0NWqoSLmi4W5zKgCDPKgWAh6UBO5gIMDWdUYnlthuiqxg9VX7U7iruNjKX6p87itP3Flhj9X0d+MGaT+dOc+tPfXxA18OjT8Gf5s7eaLwY9+aO4N7o7urNv117sw/73pXnpP/8l3XtdDlt5984jS/HyTne1cdkp7PHnrl0Eu754dP9F3773sn8aPRyWdK0t82bvz3nlLm+p/WvtX5831rz185d+bApZcX/vGlH6LQa984tO7gt8efP7X+5UyPlEq8ve7C/ZdnCv6D73Q/+nQS9W+v+vWuI6dHsgsPVFbknvtlrDZzdXwufDwfqd/d/uFI9pmGivUTB9YMw6l27w/mxgrgnkLwg5rr5z6z99mzX3zj3NcuzOMHI796PPRsZeA/+69sqvnz4I9e/Wri7s1n37w7dG1v/9nXB5W4d99HrsffG5uc2/X18EVpj7DmU68+eGV60yMzez47saG25kj4jovrrpN3L05/uOvA9MJ9p44Jb7Xd8bopXrrn+zP7Z2OrFnbOX10AX/j76j2PZN5IFX43UfXHbKbmfNM7v/9NU9dUz9VLsfYLh9OX3zzZM6J/s/l/mSe3f+sP7JaN9acn36/64HPfmbj4Gnnx/eP5Tl/3vs//5OBTN+warK64L5/79NCqior/AwjHnq8=
@@ -0,0 +1 @@
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
eNqFVMtOHEcUjaWs/BVXvcFY3fMKzMBsEmwT4mDACkSJZUejmu473eWpripXVQ+0EYuQx35+IRCIkPNYeedNVlnkC/w1udVDwyjGsjSL6fu+55y6x+cTNJYreesVlw4Nix192OnxucEXBVr341mOLlPJ6eOd3b2TwvA3mXPa9ptNpnnDsQkXZSNWedMiM3F2OlRJ+fbW7cOA3IMxlkEfAjcRZeT2Hiz31na/Gq/jveLFzpOVg2R5c9Phwe5Sa2u83QtCCKijqTK+yZhbsMBgQaoEF4BLeMRkumGYzj71kTk7GBi0hXCW4jtkmfUfJKhd5kuwZMJkjImP5jIWRYKDROWMS5/x9Dsy48GN5jqaSbuPhqwjJizOOQzbH8SK4JLuBi/PWYq2dhydZ8gSwvjni68tmmgtpazpa10SqDK6BNk2O41P6PfnWhzT/NG6jFXCZTr9PX3JdQgJjgRzeDZzT0/uNu9e3FdSYsXW9GKMqCMm+AT/uD+bK3qEMnXZ9KTT7b6qbXulxulfTGvBY+Yzm8+tkr8Rjpo4xx/OrGOusMen1B3//ec8R2tpl192NuslfnpP2w+0ONtFQzqb/ipTLg9OH9Au0zefGx5CuwdfMgmdVmcZ2t1+u9Nf7sLG1t7/9zhttzpLlbi+pykNYfP2448Oa8F8SC/BSAmh9geFHlR4e4kHfVkIEQY1y7OvmjwSQnClr6eHgeNOIDW6qgobQhE6poQINrj7ohjC4yo1DAojKLJ+JoIy4oz0FTHeSLnLimGDq8qc+kJNUpJn1Tb9hAInKJpU5EpfwTZtZP9+DQ/ndgrB70kLGwRXasJaiBJmooJRIWevGO7YUsagDDD/ZxH2M/QJGcKIG+tAK8t9JBNUKS1yagjcVgFeCqRqJhO4o/QsSJSL4cyJNF7yvnQGz/z4I54+o8fnF6HtiTGo68DMWxg2FAiaGUY3htRF8xZxRsNSCZcZktyAJ4sN2OU5F8yAU7CNbl+Z8bchlKoAliR+HouXcFAAgwpVKKzv6IeloIF3w+yQ9QlcGytDbLYaq51eq9XqdYns+TfttXAUXrP+UDqjkqJC1Te54qEPa3APSdQSDelvo+DUJ4ItTHiRvyOFvDJX1/KzWAuVNvlc4cip6EoVEYuGl3VtlPqyUXtptDrEVqfXXmq/K5A+bF8rgrYeFlz4EwJDoeKxBTXyiJnr0RuwzgjsChmDmsRO1TzytXwq3RBVuS5c9ZRJE6gb8ISAp4PE5RzqGs1ImRysxpiPeAyO2bENoeZTGxXTMfHzcEn1QsjZmL7muVhZXV5Z7a7eRMXsMVZHauB4TuHtRrd79B/qa3Sq
@@ -1 +1 @@
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
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
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
@@ -1 +1 @@
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
@@ -1 +1 @@
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
@@ -1 +0,0 @@
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
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
@@ -1 +1 @@
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
@@ -1 +1 @@
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
@@ -0,0 +1 @@
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
@@ -0,0 +1 @@
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
eNptksFuEzEQhsWbrHrgQOPdzSat2qAIRVXUSjSqIIEL4mC8w67BaxvbGxJuhJ6R/AakibJV1AJCiAtU4siBF8jb4N0UCZUc7X/8zzf/eFIMQWkq+K0Lyg0oTIw7aDspFLzKQZvTRQYmFfHssDuY5YqujlJjpG4FAZbU1xk1qc8wT0iKKfeJyIIYG6zB6HuMOrVdv81xBu3+g+PtTgLcbD8ELV0LmD8T8fhnkQKOHcLp8pEGhaoS+7V0rLyRHAehX/fr0d6nDiEgDepyImLKE3uZvKGy5sXwnGEDi7VsP2MpGSW4nCN4oQVfHgjOoZrLLl8CSIQZHcK5uuZ4t9AGm1xP5s4Xfv8qMtAaJ3B2cv8v3PuPzsM4MnQMPDGpnUYLR+uis0U+pEQo/mWEJE4or/oiIwxmdhp+L6G0RuVzJRjqMCZeoxNFXaX98GOjerTuaad3vt3Qe3hUBmRnu2E4dzGDvRqkec2Ldr0+SC8Ko6ZX32lFjVa05x32Bpv9e9U+S/+rG3p3JIWGfwCKDjOoPyR25aeN9lar2Wxs3fUy3I529qMwDGtpA0X7G4RLgkkKiKyN7TkXqLqZPabYLt06vUSIhMHK24h4oCB2aVPMtJ0blcMFuc7fjCX8v+LqK711e1TuX5w9efoH8Hwd7A==
@@ -0,0 +1 @@
eNrtVE1rFEEQxX8yDOJBd3ZnZ3cTMxIkSFDQIJroRaTp9FRmWnu72+6eTVbJwZirwvgLTDZkJfiFiBcVvOnBPxB/jTWTXT/WqOBN8Djzuqpevap6G8MeGMuVPPKESweGMocf9uHG0MCtHKzb3O2Cy1QyODu/NMgN3z+aOadt3GhQzeu2y11WF1SmLKNc1pnqNrhcUTvLKum/H2ZAE0y/uXfFggnmUpCueFW+ruIC3W+E9Wa9GZ18PscYaBfMS6YSLtPiaXqb65qXwIqgDnYP4OIF1VpwRkuOjRtWyb0zSkqoOBd7NwF0QAXvwWMDVmMbcG/XOupyu7GDeeHTx2EXrKUpbF88PyZ3/xnmcMgsuAAydVkxaHc6u8gXhSmGeY8zZeTbkoC1QfnUKBHMCaFWg3MHGYqt428OxS8annJZPHo9gS7QtVKMYjAVhjsJ9le8W8rymhdNeYugvSiM2l6zE0etODrpnV1YOrz6QjWXsvq7CXx+TSsL39EbzgkXLPZYsV/PWrN+3G63/FNel85GnZkoDMNa1gqimUOAp4yyDAJ2kLh4LFVQ/Rlc5bTYw9F5qVKpgH3vUIpnDCSoLKfCFjvO5PCEjbR2fQ0/j7Nam7s4M4M78PnIhzv+aDv92A/r0/XWtF/zMQJwtATWNDdVLHG8C34scyFq/jJ1LCMYj8tLsNwKT/34jm8ZFUByTW5ZfhsIVkhTMH7cLNv/hkqXGZTNEsFxRRGeGoOJWpVEQle7/rfoNqJluvHrKtfXH2S578D6cRTOTDc7Ubhe87nEhZQMCO51aktieBR4dg4I5QQvzvQJSLosIPHjUrGar0xKGJKqOk24HYErKCqiNlOrxDlBcj4OcHjF2CEHQ5J8pFBC+1U1oWRaXggmaFdkM2Xc6EezjQQtUIP6TXBYVeam1WVay5QGUnLisser9sZMWsQ6ZfC6foxeX/+1mZz7k5ngdVALzp6uBJ1tHpO0C7OLly6cqNzkxOXRof9DhvNg0nC2okm7ebkWaIrWUdUNnHJUFFvh703mTxb134T+xoS2r13/Av44wVA=
+2 -8
View File
@@ -1,4 +1,4 @@
# 🦜🕸️ Companies using LangGraph
# 🦜🕸️ LangGraph Adopters
This list of companies using LangGraph and their success stories is compiled from public sources. If your company uses LangGraph, we'd love for you to share your story and add it to the list. You’re also welcome to contribute updates based on publicly available information from other companies, such as blog posts or press releases.
@@ -9,23 +9,17 @@ This list of companies using LangGraph and their success stories is compiled fro
| [AppFolio](https://www.appfolio.com/) | Real Estate | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-appfolio/) |
| [Athena Intelligence](https://www.athenaintel.com/) | Software & Technology (GenAI Native) | Research & summarization | [Case study, 2024](https://blog.langchain.dev/customers-athena-intelligence/) |
| [Captide](https://www.captide.co/) | Software & Technology (GenAI Native) | Data extraction | [Case study, 2025](https://blog.langchain.dev/how-captide-is-redefining-equity-research-with-agentic-workflows-built-on-langgraph-and-langsmith/) |
| [Cisco Outshift](https://outshift.cisco.com/) | Software & Technology | DevOps | [Blog post, 2025](https://outshift.cisco.com/blog/build-react-agent-application-for-devops-tasks-using-rest-apis) |
| [C.H. Robinson](https://www.chrobinson.com/en-us/) | Logistics | Automation | [Case study, 2025](https://blog.langchain.dev/customers-chrobinson/) |
| [Elastic](https://www.elastic.co/) | Software & Technology | Copilot for domain-specific task | [Blog post, 2025](https://www.elastic.co/blog/elastic-security-generative-ai-features) |
| [GitLab](https://about.gitlab.com/) | Software & Technology | Code generation | [Duo workflow docs](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/duo_workflow/) |
| [Inconvo](https://inconvo.ai/?ref=blog.langchain.dev) | Software & Technology | Code generation | [Case study, 2025](https://blog.langchain.dev/customers-inconvo/) |
| [Infor](https://infor.com/) | Software & Technology | GenAI embedded product experiences; customer support; copilot | [Case study, 2025](https://blog.langchain.dev/customers-infor/) |
| [Klarna](https://www.klarna.com/) | Fintech | Copilot for domain-specific task | [Case study, 2025](https://blog.langchain.dev/customers-klarna/) |
| [Komodo Health](https://www.komodohealth.com/) | Healthcare | Copilot for domain-specific task | [Blog post](https://www.komodohealth.com/perspectives/new-gen-ai-assistant-empowers-the-enterprise/) |
| [LinkedIn](https://www.linkedin.com/) | Social Media | Code generation; Search & discovery | [Blog post, 2025](https://www.linkedin.com/blog/engineering/ai/practical-text-to-sql-for-data-analytics); [Blog post, 2024](https://www.linkedin.com/blog/engineering/generative-ai/behind-the-platform-the-journey-to-create-the-linkedin-genai-application-tech-stack) |
| [Minimal](https://gominimal.ai/) | E-commerce | Customer support | [Case study, 2025](https://blog.langchain.dev/how-minimal-built-a-multi-agent-customer-support-system-with-langgraph-langsmith/) |
| [OpenRecovery](https://www.openrecovery.com/) | Healthcare | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-openrecovery/) |
| [Qodo](https://www.qodo.ai/) | Software & Technology (GenAI Native) | Code generation | [Blog post, 2025](https://www.qodo.ai/blog/why-we-chose-langgraph-to-build-our-coding-agent/) |
| [Rakuten](https://www.rakuten.com/) | E-commerce / Fintech | Copilot for domain-specific task | [Blog post, 2025](https://rakuten.today/blog/from-ai-hype-to-real-world-tools-rakuten-teams-up-with-langchain.html) |
| [Replit](https://replit.com/) | Software & Technology | Code generation | [Blog post, 2024](https://blog.langchain.dev/customers-replit/); [Breakout agent story, 2024](https://www.langchain.com/breakoutagents/replit); [Fireside chat video, 2024](https://www.youtube.com/watch?v=ViykMqljjxU) |
| [Rexera](https://www.rexera.com/) | Real Estate (GenAI Native) | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-rexera/) |
| [Tradestack](https://www.tradestack.uk/) | Software & Technology (GenAI Native) | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-tradestack/) |
| [Uber](https://www.uber.com/) | Transportation | Developer productivity; Code generation | [Presentation, 2024](https://dpe.org/sessions/ty-smith-adam-huda/this-year-in-ubers-ai-driven-developer-productivity-revolution/); [Video, 2024](https://www.youtube.com/watch?v=8rkA5vWUE4Y) |
| [Unify](https://www.unifygtm.com/) | Software & Technology (GenAI Native) | Copilot for domain-specific task | [Blog post, 2024](https://blog.langchain.dev/unify-launches-agents-for-account-qualification-using-langgraph-and-langsmith/) |
| [Vizient](https://www.vizientinc.com/) | Healthcare | Copilot for domain-specific task | [Case study, 2025](https://blog.langchain.dev/p/3d2cd58c-13a5-4df9-bd84-7d54ed0ed82c/) |
| [Vodafone](https://www.vodafone.com/) | Telecommunications | Code generation; internal search | [Case study, 2025](https://blog.langchain.dev/customers-vodafone/) |
| [Vizient](https://www.vizientinc.com/) | Healthcare | Copilot for domain-specific task | [Case study, 2025](https://blog.langchain.dev/p/3d2cd58c-13a5-4df9-bd84-7d54ed0ed82c/) |
-218
View File
@@ -1,218 +0,0 @@
---
search:
boost: 2
tags:
- agent
hide:
- tags
---
# Agents
## What is an agent?
An *agent* consists of three components: a **large language model (LLM)**, a set of **tools** it can use, and a **prompt** that provides instructions.
The LLM operates in a loop. In each iteration, it selects a tool to invoke, provides input, receives the result (an observation), and uses that observation to inform the next action. The loop continues until a stopping condition is met — typically when the agent has gathered enough information to respond to the user.
<figure markdown="1">
![image](./assets/agent.png){: style="max-height:400px"}
<figcaption>Agent loop: the LLM selects tools and uses their outputs to fulfill a user request.</figcaption>
</figure>
## Basic configuration
Use [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] to instantiate an agent:
```python
from langgraph.prebuilt import create_react_agent
def get_weather(city: str) -> str: # (1)!
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
agent = create_react_agent(
model="anthropic:claude-3-7-sonnet-latest", # (2)!
tools=[get_weather], # (3)!
prompt="You are a helpful assistant" # (4)!
)
# Run the agent
agent.invoke(
{"messages": [{"role": "user", "content": "what is the weather in sf"}]}
)
```
1. Define a tool for the agent to use. Tools can be defined as vanilla Python functions. For more advanced tool usage and customization, check the [tools](./tools.md) page.
2. Provide a language model for the agent to use. To learn more about configuring language models for the agents, check the [models](./models.md) page.
3. Provide a list of tools for the model to use.
4. Provide a system prompt (instructions) to the language model used by the agent.
## LLM configuration
Use [init_chat_model](https://python.langchain.com/api_reference/langchain/chat_models/langchain.chat_models.base.init_chat_model.html) to configure an LLM with specific parameters,
such as temperature:
```python
from langchain.chat_models import init_chat_model
from langgraph.prebuilt import create_react_agent
# highlight-next-line
model = init_chat_model(
"anthropic:claude-3-7-sonnet-latest",
# highlight-next-line
temperature=0
)
agent = create_react_agent(
# highlight-next-line
model=model,
tools=[get_weather],
)
```
See the [models](./models.md) page for more information on how to configure LLMs.
## Custom Prompts
Prompts instruct the LLM how to behave. They can be:
* **Static**: A string is interpreted as a **system message**
* **Dynamic**: a list of messages generated at **runtime** based on input or configuration
### Static prompts
Define a fixed prompt string or list of messages.
```python
from langgraph.prebuilt import create_react_agent
agent = create_react_agent(
model="anthropic:claude-3-7-sonnet-latest",
tools=[get_weather],
# A static prompt that never changes
# highlight-next-line
prompt="Never answer questions about the weather."
)
agent.invoke(
{"messages": [{"role": "user", "content": "what is the weather in sf"}]}
)
```
### Dynamic prompts
Define a function that returns a message list based on the agent's state and configuration:
```python
from langchain_core.messages import AnyMessage
from langchain_core.runnables import RunnableConfig
from langgraph.prebuilt.chat_agent_executor import AgentState
from langgraph.prebuilt import create_react_agent
# highlight-next-line
def prompt(state: AgentState, config: RunnableConfig) -> list[AnyMessage]: # (1)!
user_name = config["configurable"].get("user_name")
system_msg = f"You are a helpful assistant. Address the user as {user_name}."
return [{"role": "system", "content": system_msg}] + state["messages"]
agent = create_react_agent(
model="anthropic:claude-3-7-sonnet-latest",
tools=[get_weather],
# highlight-next-line
prompt=prompt
)
agent.invoke(
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
# highlight-next-line
config={"configurable": {"user_name": "John Smith"}}
)
```
1. Dynamic prompts allow including non-message [context](./context.md) when constructing an input to the LLM, such as:
- Information passed at runtime, like a `user_id` or API credentials (using `config`).
- Internal agent state updated during a multi-step reasoning process (using `state`).
Dynamic prompts can be defined as functions that take `state` and `config` and return a list of messages to send to the LLM.
See the [context](./context.md) page for more information.
## Memory
To allow multi-turn conversations with an agent, you need to enable [persistence](../concepts/persistence.md) by providing a `checkpointer` when creating an agent. At runtime you need to provide a config containing `thread_id` — a unique identifier for the conversation (session):
```python
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import InMemorySaver
# highlight-next-line
checkpointer = InMemorySaver()
agent = create_react_agent(
model="anthropic:claude-3-7-sonnet-latest",
tools=[get_weather],
# highlight-next-line
checkpointer=checkpointer # (1)!
)
# Run the agent
# highlight-next-line
config = {"configurable": {"thread_id": "1"}}
sf_response = agent.invoke(
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
# highlight-next-line
config # (2)!
)
ny_response = agent.invoke(
{"messages": [{"role": "user", "content": "what about new york?"}]},
# highlight-next-line
config
)
```
1. `checkpointer` allows the agent to store its state at every step in the tool calling loop. This enables [short-term memory](./memory.md#short-term-memory) and [human-in-the-loop](./human-in-the-loop.md) capabilities.
2. Pass configuration with `thread_id` to be able to resume the same conversation on future agent invocations.
When you enable the checkpointer, it stores agent state at every step in the provided checkpointer database (or in memory, if using `InMemorySaver`).
Note that in the above example, when the agent is invoked the second time with the same `thread_id`, the original message history from the first conversation is automatically included, together with the new user input.
Please see the [memory guide](./memory.md) for more details on how to work with memory.
## Structured output
To produce structured responses conforming to a schema, use the `response_format` parameter. The schema can be defined with a `Pydantic` model or `TypedDict`. The result will be accessible via the `structured_response` field.
```python
from pydantic import BaseModel
from langgraph.prebuilt import create_react_agent
class WeatherResponse(BaseModel):
conditions: str
agent = create_react_agent(
model="anthropic:claude-3-7-sonnet-latest",
tools=[get_weather],
# highlight-next-line
response_format=WeatherResponse # (1)!
)
response = agent.invoke(
{"messages": [{"role": "user", "content": "what is the weather in sf"}]}
)
# highlight-next-line
response["structured_response"]
```
1. When `response_format` is provided, a separate step is added at the end of the agent loop: agent message history is passed to an LLM with structured output to generate a structured response.
To provide a system prompt to this LLM, use a tuple `(prompt, schema)`, e.g., `response_format=(prompt, WeatherResponse)`.
!!! Note "LLM post-processing"
Structured output requires an additional call to the LLM to format the response according to the schema.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

-236
View File
@@ -1,236 +0,0 @@
---
search:
boost: 2
tags:
- agent
hide:
- tags
---
# Context
Agents often require more than a list of messages to function effectively. They need **context**.
Context includes *any* data outside the message list that can shape agent behavior or tool execution. This can be:
- Information passed at runtime, like a `user_id` or API credentials.
- Internal state updated during a multi-step reasoning process.
- Persistent memory or facts from previous interactions.
LangGraph provides **three** primary ways to supply context:
| Type | Description | Mutable? | Lifetime |
|------------------------------------------------------------------------------|-----------------------------------------------|----------|-------------------------|
| [**Config**](#config-static-context) | data passed at the start of a run | ❌ | per run |
| [**State**](#state-mutable-context) | dynamic data that can change during execution | ✅ | per run or conversation |
| [**Long-term Memory (Store)**](#long-term-memory-cross-conversation-context) | data that can be shared between conversations | ✅ | across conversations |
You can use context to:
- Adjust the system prompt the model sees
- Feed tools with necessary inputs
- Track facts during an ongoing conversation
## Providing Runtime Context
Use this when you need to inject data into an agent at runtime.
### Config (static context)
Config is for immutable data like user metadata or API keys. Use
when you have values that don't change mid-run.
Specify configuration using a key called **"configurable"** which is reserved
for this purpose:
```python
agent.invoke(
{"messages": [{"role": "user", "content": "hi!"}]},
# highlight-next-line
config={"configurable": {"user_id": "user_123"}}
)
```
### State (mutable context)
State acts as short-term memory during a run. It holds dynamic data that can evolve during execution, such as values derived from tools or LLM outputs.
```python
class CustomState(AgentState):
# highlight-next-line
user_name: str
agent = create_react_agent(
# Other agent parameters...
# highlight-next-line
state_schema=CustomState,
)
agent.invoke({
"messages": "hi!",
"user_name": "Jane"
})
```
!!! tip "Turning on memory"
Please see the [memory guide](./memory.md) for more details on how to enable memory. This is a powerful feature that allows you to persist the agent's state across multiple invocations.
Otherwise, the state is scoped only to a single agent run.
### Long-Term Memory (cross-conversation context)
For context that spans *across* conversations or sessions, LangGraph allows access to **long-term memory** via a `store`. This can be used to read or update persistent facts (e.g., user profiles, preferences, prior interactions). For more, see the [Memory guide](./memory.md).
## Customizing Prompts with Context { #prompts }
Prompts define how the agent behaves. To incorporate runtime context, you can dynamically generate prompts based on the agent's state or config.
Common use cases:
- Personalization
- Role or goal customization
- Conditional behavior (e.g., user is admin)
=== "Using config"
```python
from langchain_core.messages import AnyMessage
from langchain_core.runnables import RunnableConfig
from langgraph.prebuilt import create_react_agent
from langgraph.prebuilt.chat_agent_executor import AgentState
def prompt(
state: AgentState,
# highlight-next-line
config: RunnableConfig,
) -> list[AnyMessage]:
# highlight-next-line
user_name = config["configurable"].get("user_name")
system_msg = f"You are a helpful assistant. User's name is {user_name}"
return [{"role": "system", "content": system_msg}] + state["messages"]
agent = create_react_agent(
model="anthropic:claude-3-7-sonnet-latest",
tools=[get_weather],
# highlight-next-line
prompt=prompt
)
agent.invoke(
...,
# highlight-next-line
config={"configurable": {"user_name": "John Smith"}}
)
```
=== "Using state"
```python
from langchain_core.messages import AnyMessage
from langchain_core.runnables import RunnableConfig
from langgraph.prebuilt import create_react_agent
from langgraph.prebuilt.chat_agent_executor import AgentState
class CustomState(AgentState):
# highlight-next-line
user_name: str
def prompt(
# highlight-next-line
state: CustomState
) -> list[AnyMessage]:
# highlight-next-line
user_name = state["user_name"]
system_msg = f"You are a helpful assistant. User's name is {user_name}"
return [{"role": "system", "content": system_msg}] + state["messages"]
agent = create_react_agent(
model="anthropic:claude-3-7-sonnet-latest",
tools=[...],
# highlight-next-line
state_schema=CustomState,
# highlight-next-line
prompt=prompt
)
agent.invoke({
"messages": "hi!",
# highlight-next-line
"user_name": "John Smith"
})
```
## Accessing Context in Tools { #tools }
Tools can access context through special parameter **annotations**.
* Use `RunnableConfig` for config access
* Use `Annotated[StateSchema, InjectedState]` for agent state
!!! tip
These annotations prevent LLMs from attempting to fill in the values. These parameters will be **hidden** from the LLM.
=== "Using config"
```python
def get_user_info(
# highlight-next-line
config: RunnableConfig,
) -> str:
"""Look up user info."""
# highlight-next-line
user_id = config["configurable"].get("user_id")
return "User is John Smith" if user_id == "user_123" else "Unknown user"
agent = create_react_agent(
model="anthropic:claude-3-7-sonnet-latest",
tools=[get_user_info],
)
agent.invoke(
{"messages": [{"role": "user", "content": "look up user information"}]},
# highlight-next-line
config={"configurable": {"user_id": "user_123"}}
)
```
=== "Using State"
```python
from typing import Annotated
from langgraph.prebuilt import InjectedState
class CustomState(AgentState):
# highlight-next-line
user_id: str
def get_user_info(
# highlight-next-line
state: Annotated[CustomState, InjectedState]
) -> str:
"""Look up user info."""
# highlight-next-line
user_id = state["user_id"]
return "User is John Smith" if user_id == "user_123" else "Unknown user"
agent = create_react_agent(
model="anthropic:claude-3-7-sonnet-latest",
tools=[get_user_info],
# highlight-next-line
state_schema=CustomState,
)
agent.invoke({
"messages": "look up user information",
# highlight-next-line
"user_id": "user_123"
})
```
### Update Context from Tools
Tools can update agent's context (state and long-term memory) during execution. This is useful for persisting intermediate results or making information accessible to subsequent tools or prompts. See [Memory](./memory.md#read-short-term) guide for more information.
-92
View File
@@ -1,92 +0,0 @@
---
search:
boost: 2
tags:
- agent
hide:
- tags
---
# Deployment
To deploy your LangGraph agent, create and configure a LangGraph app. This setup supports both local development and production deployments.
Features:
* 🖥️ Local server for development
* 🧩 Studio Web UI for visual debugging
* ☁️ Cloud and 🔧 self-hosted deployment options
* 📊 LangSmith integration for tracing and observability
!!! info "Requirements"
- ✅ You **must** have a [LangSmith account](https://www.langchain.com/langsmith). You can sign up for **free** and get started with the free tier.
## Create a LangGraph app
```bash
pip install -U "langgraph-cli[inmem]"
langgraph new path/to/your/app --template new-langgraph-project-python
```
This will create an empty LangGraph project. You can modify it by replacing the code in `src/agent/graph.py` with your agent code. For example:
```python
from langgraph.prebuilt import create_react_agent
def get_weather(city: str) -> str:
"""Get weather for a given city."""
return f"It's always sunny in {city}!"
graph = create_react_agent(
model="anthropic:claude-3-7-sonnet-latest",
tools=[get_weather],
prompt="You are a helpful assistant"
)
```
### Install dependencies
In the root of your new LangGraph app, install the dependencies in `edit` mode so your local changes are used by the server:
```shell
pip install -e .
```
### Create an `.env` file
You will find a `.env.example` in the root of your new LangGraph app. Create
a `.env` file in the root of your new LangGraph app and copy the contents of the `.env.example` file into it, filling in the necessary API keys:
```bash
LANGSMITH_API_KEY=lsv2...
ANTHROPIC_API_KEY=sk-
```
## Launch LangGraph server locally
```shell
langgraph dev
```
This will start up the LangGraph API server locally. If this runs successfully, you should see something like:
> Ready!
>
> - API: [http://localhost:2024](http://localhost:2024/)
>
> - Docs: http://localhost:2024/docs
>
> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
See this [tutorial](https://langchain-ai.github.io/langgraph/tutorials/langgraph-platform/local-server/) to learn more about running LangGraph app locally.
## LangGraph Studio Web UI
LangGraph Studio Web is a specialized UI that you can connect to LangGraph API server to enable visualization, interaction, and debugging of your application locally. Test your graph in the LangGraph Studio Web UI by visiting the URL provided in the output of the `langgraph dev` command.
> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
## Deployment
Once your LangGraph app is running locally, you can deploy it using LangGraph Cloud or self-hosted options. Refer to the [deployment options guide](../tutorials/deployment.md) for detailed instructions on all supported deployment models.
-128
View File
@@ -1,128 +0,0 @@
---
search:
boost: 2
tags:
- agent
hide:
- tags
---
# Evals
To evaluate your agent's performance you can use `LangSmith` [evaluations](https://docs.smith.langchain.com/evaluation). You would need to first define an evaluator function to judge the results from an agent, such as final outputs or trajectory. Depending on your evaluation technique, this may or may not involve a reference output:
```python
def evaluator(*, outputs: dict, reference_outputs: dict):
# compare agent outputs against reference outputs
output_messages = outputs["messages"]
reference_messages = reference["messages"]
score = compare_messages(output_messages, reference_messages)
return {"key": "evaluator_score", "score": score}
```
To get started, you can use prebuilt evaluators from `AgentEvals` package:
```bash
pip install -U agentevals
```
## Create evaluator
A common way to evaluate agent performance is by comparing its trajectory (the order in which it calls its tools) against a reference trajectory:
```python
import json
# highlight-next-line
from agentevals.trajectory.match import create_trajectory_match_evaluator
outputs = [
{
"role": "assistant",
"tool_calls": [
{
"function": {
"name": "get_weather",
"arguments": json.dumps({"city": "san francisco"}),
}
},
{
"function": {
"name": "get_directions",
"arguments": json.dumps({"destination": "presidio"}),
}
}
],
}
]
reference_outputs = [
{
"role": "assistant",
"tool_calls": [
{
"function": {
"name": "get_weather",
"arguments": json.dumps({"city": "san francisco"}),
}
},
],
}
]
# Create the evaluator
evaluator = create_trajectory_match_evaluator(
# highlight-next-line
trajectory_match_mode="superset", # (1)!
)
# Run the evaluator
result = evaluator(
outputs=outputs, reference_outputs=reference_outputs
)
```
1. Specify how the trajectories will be compared. `superset` will accept output trajectory as valid if it's a superset of the reference one. Other options include: [strict](https://github.com/langchain-ai/agentevals?tab=readme-ov-file#strict-match), [unordered](https://github.com/langchain-ai/agentevals?tab=readme-ov-file#unordered-match) and [subset](https://github.com/langchain-ai/agentevals?tab=readme-ov-file#subset-and-superset-match)
As a next step, learn more about how to [customize trajectory match evaluator](https://github.com/langchain-ai/agentevals?tab=readme-ov-file#agent-trajectory-match).
### LLM-as-a-judge
You can use LLM-as-a-judge evaluator that uses an LLM to compare the trajectory against the reference outputs and output a score:
```python
import json
from agentevals.trajectory.llm import (
# highlight-next-line
create_trajectory_llm_as_judge,
TRAJECTORY_ACCURACY_PROMPT_WITH_REFERENCE
)
evaluator = create_trajectory_llm_as_judge(
prompt=TRAJECTORY_ACCURACY_PROMPT_WITH_REFERENCE,
model="openai:o3-mini"
)
```
## Run evaluator
To run an evaluator, you will first need to create a [LangSmith dataset](https://docs.smith.langchain.com/evaluation/concepts#datasets). To use the prebuilt AgentEvals evaluators, you will need a dataset with the following schema:
- **input**: `{"messages": [...]}` input messages to call the agent with.
- **output**: `{"messages": [...]}` expected message history in the agent output. For trajectory evaluation, you can choose to keep only assistant messages.
```python
from langsmith import Client
from langgraph.prebuilt import create_react_agent
from agentevals.trajectory.match import create_trajectory_match_evaluator
client = Client()
agent = create_react_agent(...)
evaluator = create_trajectory_match_evaluator(...)
experiment_results = client.evaluate(
lambda inputs: agent.invoke(inputs),
# replace with your dataset name
data="<Name of your dataset>",
evaluators=[evaluator]
)
```
-238
View File
@@ -1,238 +0,0 @@
---
search:
boost: 2
tags:
- human-in-the-loop
- hil
- agent
hide:
- tags
---
# Human-in-the-loop
To review, edit and approve tool calls in an agent you can use LangGraph's built-in [Human-In-the-Loop (HIL)](../concepts/human_in_the_loop.md) features, specifically the [`interrupt()`][langgraph.types.interrupt] primitive.
LangGraph allows you to pause execution **indefinitely** — for minutes, hours, or even days—until human input is received.
This is possible because the agent state is **checkpointed into a database**, which allows the system to persist execution context and later resume the workflow, continuing from where it left off.
For a deeper dive into the **human-in-the-loop** concept, see the [concept guide](../concepts/human_in_the_loop.md).
<figure markdown="1">
![image](../concepts/img/human_in_the_loop/tool-call-review.png){: style="max-height:400px"}
<figcaption>
A human can review and edit the output from the agent before proceeding. This is particularly critical in applications where the tool calls requested may be sensitive or require human oversight.
</figcaption>
</figure>
## Review tool calls
To add a human approval step to a tool:
1. Use `interrupt()` in the tool to pause execution.
2. Resume with a `Command(resume=...)` to continue based on human input.
```python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt
from langgraph.prebuilt import create_react_agent
# An example of a sensitive tool that requires human review / approval
def book_hotel(hotel_name: str):
"""Book a hotel"""
# highlight-next-line
response = interrupt( # (1)!
f"Trying to call `book_hotel` with args {{'hotel_name': {hotel_name}}}. "
"Please approve or suggest edits."
)
if response["type"] == "accept":
pass
elif response["type"] == "edit":
hotel_name = response["args"]["hotel_name"]
else:
raise ValueError(f"Unknown response type: {response['type']}")
return f"Successfully booked a stay at {hotel_name}."
# highlight-next-line
checkpointer = InMemorySaver() # (2)!
agent = create_react_agent(
model="anthropic:claude-3-5-sonnet-latest",
tools=[book_hotel],
# highlight-next-line
checkpointer=checkpointer, # (3)!
)
```
1. The [`interrupt` function][langgraph.types.interrupt] pauses the agent graph at a specific node. In this case, we call `interrupt()` at the beginning of the tool function, which pauses the graph at the node that executes the tool. The information inside `interrupt()` (e.g., tool calls) can be presented to a human, and the graph can be resumed with the user input (tool call approval, edit or feedback).
2. The `InMemorySaver` is used to store the agent state at every step in the tool calling loop. This enables [short-term memory](./memory.md#short-term-memory) and [human-in-the-loop](./human-in-the-loop.md) capabilities. In this example, we use `InMemorySaver` to store the agent state in memory. In a production application, the agent state will be stored in a database.
3. Initialize the agent with the `checkpointer`.
Run the agent with the `stream()` method, passing the `config` object to specify the thread ID. This allows the agent to resume the same conversation on future invocations.
```python
config = {
"configurable": {
# highlight-next-line
"thread_id": "1"
}
}
for chunk in agent.stream(
{"messages": [{"role": "user", "content": "book a stay at McKittrick hotel"}]},
# highlight-next-line
config
):
print(chunk)
print("\n")
```
> You should see that the agent runs until it reaches the `interrupt()` call, at which point it pauses and waits for human input.
Resume the agent with a `Command(resume=...)` to continue based on human input.
```python
from langgraph.types import Command
for chunk in agent.stream(
# highlight-next-line
Command(resume={"type": "accept"}), # (1)!
# Command(resume={"type": "edit", "args": {"hotel_name": "McKittrick Hotel"}}),
config
):
print(chunk)
print("\n")
```
1. The [`interrupt` function][langgraph.types.interrupt] is used in conjunction with the [`Command`](../reference/types.md#langgraph.types.Command) object to resume the graph with a value provided by the human.
## Using with Agent Inbox
You can create a wrapper to add interrupts to *any* tool.
The example below provides a reference implementation compatible with [Agent Inbox UI](https://github.com/langchain-ai/agent-inbox) and [Agent Chat UI](https://github.com/langchain-ai/agent-chat-ui).
```python title="Wrapper that adds human-in-the-loop to any tool"
from typing import Callable
from langchain_core.tools import BaseTool, tool as create_tool
from langchain_core.runnables import RunnableConfig
from langgraph.types import interrupt
from langgraph.prebuilt.interrupt import HumanInterruptConfig, HumanInterrupt
def add_human_in_the_loop(
tool: Callable | BaseTool,
*,
interrupt_config: HumanInterruptConfig = None,
) -> BaseTool:
"""Wrap a tool to support human-in-the-loop review."""
if not isinstance(tool, BaseTool):
tool = create_tool(tool)
if interrupt_config is None:
interrupt_config = {
"allow_accept": True,
"allow_edit": True,
"allow_respond": True,
}
@create_tool( # (1)!
tool.name,
description=tool.description,
args_schema=tool.args_schema
)
def call_tool_with_interrupt(config: RunnableConfig, **tool_input):
request: HumanInterrupt = {
"action_request": {
"action": tool.name,
"args": tool_input
},
"config": interrupt_config,
"description": "Please review the tool call"
}
# highlight-next-line
response = interrupt([request])[0] # (2)!
# approve the tool call
if response["type"] == "accept":
tool_response = tool.invoke(tool_input, config)
# update tool call args
elif response["type"] == "edit":
tool_input = response["args"]["args"]
tool_response = tool.invoke(tool_input, config)
# respond to the LLM with user feedback
elif response["type"] == "response":
user_feedback = response["args"]
tool_response = user_feedback
else:
raise ValueError(f"Unsupported interrupt response type: {response['type']}")
return tool_response
return call_tool_with_interrupt
```
1. This wrapper creates a new tool that calls `interrupt()` **before** executing the wrapped tool.
2. `interrupt()` is using special input and output format that's expected by [Agent Inbox UI](https://github.com/langchain-ai/agent-inbox):
- a list of [`HumanInterrupt`][langgraph.prebuilt.interrupt.HumanInterrupt] objects is sent to `AgentInbox` render interrupt information to the end user
- resume value is provided by `AgentInbox` as a list (i.e., `Command(resume=[...])`)
You can use the `add_human_in_the_loop` wrapper to add `interrupt()` to any tool without having to add it *inside* the tool:
```python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.prebuilt import create_react_agent
# highlight-next-line
checkpointer = InMemorySaver()
def book_hotel(hotel_name: str):
"""Book a hotel"""
return f"Successfully booked a stay at {hotel_name}."
agent = create_react_agent(
model="anthropic:claude-3-5-sonnet-latest",
tools=[
# highlight-next-line
add_human_in_the_loop(book_hotel), # (1)!
],
# highlight-next-line
checkpointer=checkpointer,
)
config = {"configurable": {"thread_id": "1"}}
# Run the agent
for chunk in agent.stream(
{"messages": [{"role": "user", "content": "book a stay at McKittrick hotel"}]},
# highlight-next-line
config
):
print(chunk)
print("\n")
```
1. The `add_human_in_the_loop` wrapper is used to add `interrupt()` to the tool. This allows the agent to pause execution and wait for human input before proceeding with the tool call.
> You should see that the agent runs until it reaches the `interrupt()` call,
> at which point it pauses and waits for human input.
Resume the agent with a `Command(resume=...)` to continue based on human input.
```python
from langgraph.types import Command
for chunk in agent.stream(
# highlight-next-line
Command(resume=[{"type": "accept"}]),
# Command(resume=[{"type": "edit", "args": {"args": {"hotel_name": "McKittrick Hotel"}}}]),
config
):
print(chunk)
print("\n")
```
## Additional resources
* [Human-in-the-loop in LangGraph](../concepts/human_in_the_loop.md)
-107
View File
@@ -1,107 +0,0 @@
---
search:
boost: 2
tags:
- agent
hide:
- tags
---
# MCP Integration
[Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open protocol that standardizes how applications provide tools and context to language models. LangGraph agents can use tools defined on MCP servers through the `langchain-mcp-adapters` library.
![MCP](./assets/mcp.png)
Install the `langchain-mcp-adapters` library to use MCP tools in LangGraph:
```bash
pip install langchain-mcp-adapters
```
## Use MCP tools
The `langchain-mcp-adapters` package enables agents to use tools defined across one or more MCP servers.
```python title="Agent using tools defined on MCP servers"
# highlight-next-line
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
# highlight-next-line
async with MultiServerMCPClient(
{
"math": {
"command": "python",
# Replace with absolute path to your math_server.py file
"args": ["/path/to/math_server.py"],
"transport": "stdio",
},
"weather": {
# Ensure your start your weather server on port 8000
"url": "http://localhost:8000/sse",
"transport": "sse",
}
}
) as client:
agent = create_react_agent(
"anthropic:claude-3-7-sonnet-latest",
# highlight-next-line
client.get_tools()
)
math_response = await agent.ainvoke(
{"messages": [{"role": "user", "content": "what's (3 + 5) x 12?"}]}
)
weather_response = await agent.ainvoke(
{"messages": [{"role": "user", "content": "what is the weather in nyc?"}]}
)
```
## Custom MCP servers
To create your own MCP servers, you can use the `mcp` library. This library provides a simple way to define tools and run them as servers.
Install the MCP library:
```bash
pip install mcp
```
Use the following reference implementations to test your agent with MCP tool servers.
```python title="Example Math Server (stdio transport)"
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Math")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
@mcp.tool()
def multiply(a: int, b: int) -> int:
"""Multiply two numbers"""
return a * b
if __name__ == "__main__":
mcp.run(transport="stdio")
```
```python title="Example Weather Server (SSE transport)"
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Weather")
@mcp.tool()
async def get_weather(location: str) -> str:
"""Get weather for location."""
return "It's always sunny in New York"
if __name__ == "__main__":
mcp.run(transport="sse")
```
## Additional resources
- [MCP documentation](https://modelcontextprotocol.io/introduction)
- [MCP Transport documentation](https://modelcontextprotocol.io/docs/concepts/transports)

Some files were not shown because too many files have changed in this diff Show More