mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-18 05:35:43 +02:00
Compare commits
171
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4875973ac5 | ||
|
|
1393270664 | ||
|
|
6e0295b4de | ||
|
|
4aadfccf95 | ||
|
|
70b2da1301 | ||
|
|
209864da45 | ||
|
|
8b6ef35f0c | ||
|
|
a01537d1a5 | ||
|
|
05b4a30c04 | ||
|
|
93e10fbe15 | ||
|
|
f6989f2c7d | ||
|
|
c4f8346479 | ||
|
|
82c9d4b368 | ||
|
|
647f22fdd9 | ||
|
|
0aba1b4887 | ||
|
|
4c0c52d996 | ||
|
|
580fe68c8e | ||
|
|
2f26268ff4 | ||
|
|
84f8a43f6e | ||
|
|
7ce0e3e15e | ||
|
|
1e6d958434 | ||
|
|
68ba8aa393 | ||
|
|
e6a0f08561 | ||
|
|
f38784a291 | ||
|
|
fc834127fd | ||
|
|
f3a0cbf294 | ||
|
|
f4fec76257 | ||
|
|
457641f15e | ||
|
|
9f15e15e26 | ||
|
|
cb9989030e | ||
|
|
a008725c06 | ||
|
|
a93f17e624 | ||
|
|
c7eddcc6e3 | ||
|
|
4cdad6c206 | ||
|
|
b9fb155d59 | ||
|
|
8b817a5b16 | ||
|
|
3a67f3a3eb | ||
|
|
1283539500 | ||
|
|
69ad42cac5 | ||
|
|
264b02e3ad | ||
|
|
b5c659bc9f | ||
|
|
4623f7b5da | ||
|
|
1641402341 | ||
|
|
d03ead2f43 | ||
|
|
5a8624fdfd | ||
|
|
c44ec55095 | ||
|
|
25d682cc9e | ||
|
|
6f37330141 | ||
|
|
1356a0ba42 | ||
|
|
9786be1ff7 | ||
|
|
c2a129c882 | ||
|
|
62f004fd28 | ||
|
|
d4b22ac1d4 | ||
|
|
0415c02b40 | ||
|
|
e8665f84e7 | ||
|
|
8ff5e79e70 | ||
|
|
7f4822931e | ||
|
|
9706211aca | ||
|
|
405da6d507 | ||
|
|
bf7252cadc | ||
|
|
e33bac6737 | ||
|
|
da97d2e1ba | ||
|
|
6baf320d8e | ||
|
|
a064ccdca1 | ||
|
|
b5479b48bf | ||
|
|
9dbcb03185 | ||
|
|
f2faa39ca9 | ||
|
|
b4f6cdf01f | ||
|
|
cd976e779d | ||
|
|
c1f337f50b | ||
|
|
31d3ceaf6d | ||
|
|
a48844632d | ||
|
|
437891aa4f | ||
|
|
3e1bbd3123 | ||
|
|
9111449ffd | ||
|
|
77d7c00ce8 | ||
|
|
91725d742d | ||
|
|
d73a4539ec | ||
|
|
15e2df6da5 | ||
|
|
ce6b396186 | ||
|
|
d2ab02edf1 | ||
|
|
0aef9424a8 | ||
|
|
ed23288e5b | ||
|
|
7ec8a4cb4d | ||
|
|
2d9ca3045e | ||
|
|
b065c54871 | ||
|
|
3c0a677c90 | ||
|
|
7e4852373d | ||
|
|
422b2ba7f0 | ||
|
|
2c66ac869d | ||
|
|
1ef7121100 | ||
|
|
a53287f3d8 | ||
|
|
da96925ecb | ||
|
|
f3403eab48 | ||
|
|
208d9d165d | ||
|
|
49c74dd569 | ||
|
|
2d97af57f8 | ||
|
|
b310ce07bc | ||
|
|
65976f311f | ||
|
|
80c9d61fbd | ||
|
|
a578c7b137 | ||
|
|
04e8342d97 | ||
|
|
b0e11ae524 | ||
|
|
d45253cee8 | ||
|
|
1377e3b6ba | ||
|
|
c36323cba8 | ||
|
|
148cf52981 | ||
|
|
9010303245 | ||
|
|
661476e88d | ||
|
|
3397d8908f | ||
|
|
e005f0472b | ||
|
|
18a1d60e45 | ||
|
|
5091d5e9fe | ||
|
|
da1fae0c72 | ||
|
|
7db81d72dc | ||
|
|
4ce387ee28 | ||
|
|
b902db686d | ||
|
|
16be48079e | ||
|
|
cf3a9ad63e | ||
|
|
547830ef24 | ||
|
|
d333e52fce | ||
|
|
15fe44ddf8 | ||
|
|
b6aff521e5 | ||
|
|
35ff9f5211 | ||
|
|
d690fc3e00 | ||
|
|
3325787af7 | ||
|
|
77a3cffa7f | ||
|
|
d1f2a6c518 | ||
|
|
b984584851 | ||
|
|
bce4545021 | ||
|
|
29c317887d | ||
|
|
84a0eca935 | ||
|
|
d9ed1ef52e | ||
|
|
fda79e00ce | ||
|
|
3163a60466 | ||
|
|
ac05955222 | ||
|
|
f3fe30380c | ||
|
|
2c5ddceeb1 | ||
|
|
98976e016a | ||
|
|
ea8025b719 | ||
|
|
735a76a16c | ||
|
|
a33437964c | ||
|
|
cb9405bcee | ||
|
|
bd2268404c | ||
|
|
25f88740c1 | ||
|
|
e9809ae9c1 | ||
|
|
69311a4135 | ||
|
|
bb3193c83e | ||
|
|
7a2eb614dc | ||
|
|
7fd6931200 | ||
|
|
cbca07e3db | ||
|
|
066525b335 | ||
|
|
a1dad43602 | ||
|
|
1e8f097656 | ||
|
|
db26c915a9 | ||
|
|
9b62280fc5 | ||
|
|
515ad8d7a6 | ||
|
|
d378f0e06a | ||
|
|
740870df65 | ||
|
|
6e20c9f3f9 | ||
|
|
530544234a | ||
|
|
f11d241482 | ||
|
|
e81979827f | ||
|
|
2064ea4793 | ||
|
|
16cfeff78c | ||
|
|
4321d337d6 | ||
|
|
d56e2545a6 | ||
|
|
9a3f96c459 | ||
|
|
cb509ad6a5 | ||
|
|
e479a2c643 | ||
|
|
0caae32a40 |
@@ -20,7 +20,30 @@ env:
|
||||
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/**'
|
||||
sdk-js:
|
||||
- 'libs/sdk-js/**'
|
||||
|
||||
lint:
|
||||
needs: changes
|
||||
name: cd ${{ matrix.working-directory }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -34,12 +57,14 @@ jobs:
|
||||
"libs/checkpoint-postgres",
|
||||
"libs/scheduler-kafka",
|
||||
]
|
||||
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:
|
||||
@@ -50,6 +75,7 @@ jobs:
|
||||
"libs/checkpoint-sqlite",
|
||||
"libs/checkpoint-postgres",
|
||||
]
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
uses: ./.github/workflows/_test.yml
|
||||
with:
|
||||
working-directory: ${{ matrix.working-directory }}
|
||||
@@ -57,17 +83,23 @@ 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:
|
||||
@@ -80,11 +112,15 @@ jobs:
|
||||
run: python .github/scripts/check_sdk_methods.py
|
||||
|
||||
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:
|
||||
@@ -109,6 +145,8 @@ jobs:
|
||||
run: yarn build
|
||||
|
||||
test-js:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.sdk-js == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
with:
|
||||
skip: '*.ambr,*.lock,*.ipynb,*.yaml,*.zlib'
|
||||
skip: '*.ambr,*.lock,*.ipynb,*.yaml,*.zlib,*.md'
|
||||
ignore_words_list: ${{ steps.extract_ignore_words.outputs.ignore_words_list }}
|
||||
# We do this to avoid spellchecking cell outputs
|
||||
- name: Codespell Notebooks
|
||||
|
||||
@@ -48,6 +48,7 @@ jobs:
|
||||
deploy:
|
||||
# needs: run-changed-notebooks
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10 # Job will be cancelled if it runs for more than 10 minutes
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.MKDOCS_GITHUB_TOKEN }}
|
||||
steps:
|
||||
@@ -62,17 +63,38 @@ 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: |
|
||||
poetry install --with test --no-root
|
||||
yarn
|
||||
poetry install --with test --with docs --no-root
|
||||
poetry run pip install -U \
|
||||
pytest \
|
||||
pytest-check-links \
|
||||
langsmith \
|
||||
langchain \
|
||||
GitPython \
|
||||
"git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
|
||||
"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
|
||||
- name: Lint Docs
|
||||
# This step lints the docs using the existing linting set up.
|
||||
# It should be very fast and should not require any external services.
|
||||
@@ -83,6 +105,8 @@ jobs:
|
||||
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
|
||||
ANTHROPIC_API_KEY: sk-ant-api03-1234567890 # fake placeholder, shouldn't actually be used
|
||||
- name: Check links in notebooks
|
||||
env:
|
||||
LANGCHAIN_API_KEY: test
|
||||
@@ -100,6 +124,7 @@ jobs:
|
||||
--check-links-ignore "/.*\.(ipynb|html)$" \
|
||||
--check-links-ignore "https://python\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://openai\.com/.*" \
|
||||
--check-links-ignore "https://www\.uber\.com/.*" \
|
||||
--check-links-ignore "https://pepy\.tech/.*" \
|
||||
--check-links $(find site -name "index.html" | grep -v 'storm/index.html')
|
||||
|
||||
@@ -132,7 +157,7 @@ jobs:
|
||||
uses: actions/configure-pages@v4
|
||||
|
||||
- name: Upload Pages Artifact
|
||||
if: github.ref == 'refs/heads/main'
|
||||
# if: github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./docs/site/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import toml
|
||||
|
||||
pyproject_toml = toml.load("../libs/langgraph/pyproject.toml")
|
||||
pyproject_toml = toml.load("pyproject.toml")
|
||||
|
||||
# Extract the ignore words list (adjust the key as per your TOML structure)
|
||||
ignore_words_list = (
|
||||
|
||||
@@ -179,3 +179,4 @@ Untitled*.ipynb
|
||||
Chinook.db
|
||||
|
||||
.vercel
|
||||
.turbo
|
||||
|
||||
+20
-4
@@ -1,4 +1,4 @@
|
||||
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell build-typedoc llms-text
|
||||
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell build-typedoc llms-text build-prebuilt tests
|
||||
|
||||
build-typedoc:
|
||||
cd ../libs/sdk-js && yarn install --include-dev && yarn typedoc
|
||||
@@ -6,20 +6,36 @@ build-typedoc:
|
||||
# Add links to the monorepo
|
||||
sed -e '1,10s|@langchain/langgraph-sdk|[@langchain/langgraph-sdk](https://github.com/langchain-ai/langgraph/tree/main/libs/sdk-js)|g' docs/cloud/reference/sdk/js_ts_sdk_ref.md > temp_file && mv temp_file docs/cloud/reference/sdk/js_ts_sdk_ref.md
|
||||
|
||||
build-docs: build-typedoc
|
||||
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.
|
||||
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
|
||||
poetry run python -m mkdocs build --clean -f mkdocs.yml --strict
|
||||
|
||||
llms-text:
|
||||
poetry run python _scripts/generate_llms_text.py docs/llms-full.txt
|
||||
poetry run python -m _scripts.generate_llms_text docs/llms-full.txt
|
||||
|
||||
install-vercel-deps:
|
||||
dnf install -y python3.11
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
poetry self update 1.8.5
|
||||
# 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
|
||||
|
||||
tests:
|
||||
# Run unit tests
|
||||
poetry run pytest tests/unit_tests
|
||||
|
||||
|
||||
vercel-build-docs: install-vercel-deps
|
||||
poetry install
|
||||
make build-docs
|
||||
|
||||
|
||||
serve-clean-docs: clean-docs
|
||||
poetry run python -m mkdocs serve -c -f mkdocs.yml --strict -w ../libs/langgraph
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Add typescript translation to a given markdown file."""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
|
||||
import requests
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
|
||||
URL = "https://gist.githubusercontent.com/eyurtsev/e7486731415463a9bc5b4682358859c8/raw/b5a5fda9c7e3387cfcb781f25082814d43675d50/gistfile1.txt"
|
||||
response = requests.get(URL)
|
||||
response.raise_for_status()
|
||||
reference_snippets = response.text
|
||||
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest")
|
||||
|
||||
|
||||
def _get_tqdm():
|
||||
try:
|
||||
from tqdm import tqdm
|
||||
except ImportError:
|
||||
# If not available return a simple identity function
|
||||
def tqdm(iterable, *args, **kwargs):
|
||||
return iterable
|
||||
|
||||
return tqdm
|
||||
|
||||
|
||||
_tqdm = _get_tqdm()
|
||||
|
||||
opening_pattern = re.compile(r"^\s*```python(?:\s+.*)?\s*$")
|
||||
closing_pattern = re.compile(r"^\s*```\s*$")
|
||||
|
||||
|
||||
def extract_python_snippets(markdown: str) -> list[str]:
|
||||
"""
|
||||
Extract all python code blocks (including their fence lines) from the markdown content.
|
||||
A python block is defined as any block that starts with a line containing an opening fence
|
||||
with '```python' (optionally with extra parameters) and ends with a closing fence '```'.
|
||||
"""
|
||||
snippets = []
|
||||
inside_block = False
|
||||
current_snippet = []
|
||||
|
||||
for line in markdown.splitlines(keepends=True):
|
||||
if not inside_block:
|
||||
if opening_pattern.match(line):
|
||||
inside_block = True
|
||||
current_snippet = [line]
|
||||
else:
|
||||
current_snippet.append(line)
|
||||
if closing_pattern.match(line):
|
||||
inside_block = False
|
||||
snippets.append("".join(current_snippet))
|
||||
current_snippet = []
|
||||
return snippets
|
||||
|
||||
|
||||
def translate_snippet(python_snippet: str) -> str:
|
||||
"""Translate a python code block into a TypeScript code block using Langchain.
|
||||
The response is expected to be a properly fenced TypeScript code block (i.e.
|
||||
starting with ```typescript and ending with ```).
|
||||
"""
|
||||
ai_message = model.invoke(
|
||||
[
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
f"You have access to the following up-to-date example TypeScript code "
|
||||
f"snippets that show examples of building with langgraph "
|
||||
f"and langchain:\n\n{reference_snippets}\n\n"
|
||||
"Use this context to translate the following Python code to equivalent "
|
||||
"TypeScript. Ensure that your output is a valid fenced TypeScript "
|
||||
"code block (i.e. starts with ```typescript and ends with ```)."
|
||||
),
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Translate this Python snippet to TypeScript:\n\n{python_snippet}",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Use a regular expression to search for a TypeScript code block in the response.
|
||||
pattern = r"```typescript\s*(.*?)\s*```"
|
||||
match = re.search(pattern, ai_message.content, re.DOTALL)
|
||||
if match:
|
||||
# Reconstruct the code block with proper fences.
|
||||
typescript_code = match.group(1).strip()
|
||||
return f"```typescript\n{typescript_code}\n```"
|
||||
else:
|
||||
raise ValueError("No TypeScript code block found in the model's response.")
|
||||
|
||||
|
||||
def insert_translations_into_markdown(
|
||||
markdown: str, typescript_snippets: list[str]
|
||||
) -> str:
|
||||
"""Walks through the original markdown content and, after each
|
||||
Python snippet block, inserts the corresponding translated TypeScript snippet.
|
||||
It assumes that the ordering of the Python snippets
|
||||
(from extract_python_snippets) matches the order they appear in the markdown.
|
||||
"""
|
||||
output_lines = []
|
||||
lines = markdown.splitlines(keepends=True)
|
||||
inside_block = False
|
||||
snippet_index = 0
|
||||
|
||||
for line in lines:
|
||||
output_lines.append(line)
|
||||
if not inside_block and opening_pattern.match(line):
|
||||
# We've encountered the start of a python code block.
|
||||
inside_block = True
|
||||
elif inside_block:
|
||||
if closing_pattern.match(line):
|
||||
# End of a python snippet block.
|
||||
inside_block = False
|
||||
if snippet_index < len(typescript_snippets):
|
||||
# Insert an extra newline for clarity, then the translated TypeScript snippet.
|
||||
output_lines.append("\n")
|
||||
output_lines.append(typescript_snippets[snippet_index])
|
||||
output_lines.append("\n")
|
||||
snippet_index += 1
|
||||
return "".join(output_lines)
|
||||
|
||||
|
||||
def main(file_path: str) -> None:
|
||||
# Read the markdown file.
|
||||
with open(file_path, "r") as f:
|
||||
markdown_content = f.read()
|
||||
|
||||
# 1. Extract all Python snippets.
|
||||
python_snippets = extract_python_snippets(markdown_content)[:1]
|
||||
|
||||
# 2. Translate each Python snippet to TypeScript.
|
||||
typescript_snippets = []
|
||||
# Replace with .batch() for faster translation
|
||||
for python_snippet in _tqdm(python_snippets):
|
||||
ts_snippet = translate_snippet(python_snippet)
|
||||
typescript_snippets.append(ts_snippet)
|
||||
|
||||
# 3. Insert the TypeScript translations after their respective Python snippets.
|
||||
updated_markdown = insert_translations_into_markdown(
|
||||
markdown_content, typescript_snippets
|
||||
)
|
||||
|
||||
# Overwrite the original markdown file with the updated content.
|
||||
with open(file_path, "w") as f:
|
||||
f.write(updated_markdown)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Translate Python snippets in a markdown file to TypeScript and insert them after each Python snippet."
|
||||
)
|
||||
parser.add_argument("file_path", type=str, help="Path to the markdown file.")
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.file_path)
|
||||
@@ -250,7 +250,8 @@ def update_markdown_with_imports(markdown: str) -> str:
|
||||
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)```', re.DOTALL
|
||||
r"(?P<indent>[ \t]*)```(?P<language>python|py)\n(?P<code>.*?)\n(?P=indent)```",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
def replace_code_block(match: re.Match) -> str:
|
||||
@@ -262,9 +263,8 @@ def update_markdown_with_imports(markdown: str) -> str:
|
||||
Returns:
|
||||
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
|
||||
indent = match.group("indent")
|
||||
code_block = match.group("code")
|
||||
# Retrieve import information from the code block
|
||||
imports = get_imports(code_block, "__unused__")
|
||||
|
||||
@@ -274,11 +274,11 @@ def update_markdown_with_imports(markdown: str) -> str:
|
||||
return original_code_block
|
||||
|
||||
# Generate API reference links for each import
|
||||
api_links = ' | '.join(
|
||||
api_links = " | ".join(
|
||||
f'<a href="{imp["docs"]}">{imp["imported"]}</a>' for imp in imports
|
||||
)
|
||||
# Return the code block with appended API reference links
|
||||
return f'{original_code_block}\n\n{indent}API Reference: {api_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)
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
import glob
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from mkdocs.structure.files import File
|
||||
from mkdocs.structure.pages import Page
|
||||
|
||||
from notebook_hooks import _on_page_markdown_with_config
|
||||
from _scripts.notebook_hooks import _on_page_markdown_with_config
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
# Get source directory (parent of HERE / docs)
|
||||
|
||||
@@ -1,30 +1,266 @@
|
||||
import ast
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
import nbformat
|
||||
from nbconvert.exporters import MarkdownExporter
|
||||
from nbconvert.preprocessors import Preprocessor
|
||||
|
||||
|
||||
def _uses_input(source: str) -> bool:
|
||||
"""Parse the source code to determine if it uses the input() function."""
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except SyntaxError:
|
||||
# If there's a syntax error, assume input() might be present to be safe.
|
||||
return False
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Call):
|
||||
# Check if the function called is named 'input'
|
||||
if isinstance(node.func, ast.Name) and node.func.id == "input":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _rewrite_cell_magic(code: str) -> str:
|
||||
"""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.
|
||||
- Any other non-empty line causes a NotImplementedError.
|
||||
|
||||
Args:
|
||||
code (str): The original code block.
|
||||
|
||||
Returns:
|
||||
str: The transformed code block.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If a line doesn't start with either "%%capture" or "%pip".
|
||||
"""
|
||||
rewritten_lines = []
|
||||
|
||||
for line in code.splitlines():
|
||||
stripped = line.strip()
|
||||
# Skip empty lines
|
||||
if not stripped:
|
||||
continue
|
||||
# Ignore %%capture lines
|
||||
if stripped.startswith("%%capture"):
|
||||
continue
|
||||
# Rewrite %pip lines by dropping the '%'
|
||||
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}")
|
||||
|
||||
return "\n".join(rewritten_lines)
|
||||
|
||||
|
||||
class PrintCallVisitor(ast.NodeVisitor):
|
||||
"""
|
||||
This visitor sets self.has_print to True if it encounters a call
|
||||
to a print within the global scope.
|
||||
|
||||
This should catch calls to print(), print_stream(), etc. (Prefixed with "print").
|
||||
|
||||
May have some false positives, but it's not meant to be perfect.
|
||||
|
||||
Temporary code for notebook conversion.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.has_print = False
|
||||
self.scope_level = 0 # counter to track whether we're inside a def/lambda
|
||||
|
||||
def visit_FunctionDef(self, node):
|
||||
self.scope_level += 1
|
||||
self.generic_visit(node)
|
||||
self.scope_level -= 1
|
||||
|
||||
def visit_AsyncFunctionDef(self, node):
|
||||
self.scope_level += 1
|
||||
self.generic_visit(node)
|
||||
self.scope_level -= 1
|
||||
|
||||
def visit_Lambda(self, node):
|
||||
self.scope_level += 1
|
||||
self.generic_visit(node)
|
||||
self.scope_level -= 1
|
||||
|
||||
def visit_ClassDef(self, node):
|
||||
self.scope_level += 1
|
||||
self.generic_visit(node)
|
||||
self.scope_level -= 1
|
||||
|
||||
def visit_Call(self, node):
|
||||
# Only consider calls when not inside a function definition.
|
||||
if self.scope_level == 0:
|
||||
if isinstance(node.func, ast.Name) and node.func.id.startswith("print"):
|
||||
self.has_print = True
|
||||
self.generic_visit(node)
|
||||
|
||||
|
||||
def _has_output(source: str) -> bool:
|
||||
"""Determine if the code block is expected to produce output.
|
||||
|
||||
Args:
|
||||
source (str): The source code of the code block.
|
||||
|
||||
Returns:
|
||||
True if the code block is expected to produce output, False otherwise.
|
||||
|
||||
Must meet the following conditions:
|
||||
|
||||
1. There is a call to a printing function (name starts with "print")
|
||||
that is not inside a function definition.
|
||||
2. The last top-level statement is an expression that is valid if:
|
||||
- It is any expression (including calls) AND
|
||||
- It is NOT a call to `display(...)`.
|
||||
|
||||
`display` isn't handled currently by markdown-exec
|
||||
"""
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except SyntaxError:
|
||||
return False
|
||||
|
||||
# Condition (1): Check for a global print-like call.
|
||||
visitor = PrintCallVisitor()
|
||||
visitor.visit(tree)
|
||||
condition_a = visitor.has_print
|
||||
|
||||
# Condition (2): Check the last top-level statement.
|
||||
condition_b = False
|
||||
if tree.body:
|
||||
last_stmt = tree.body[-1]
|
||||
if isinstance(last_stmt, ast.Expr):
|
||||
# If the expression is a call, ensure it's not a call to "display"
|
||||
if isinstance(last_stmt.value, ast.Call):
|
||||
if (
|
||||
isinstance(last_stmt.value.func, ast.Name)
|
||||
and last_stmt.value.func.id == "display"
|
||||
):
|
||||
condition_b = False # exclude display-wrapped expressions
|
||||
else:
|
||||
condition_b = True
|
||||
else:
|
||||
# Any other expression qualifies.
|
||||
condition_b = True
|
||||
|
||||
return condition_a or condition_b
|
||||
|
||||
|
||||
def _convert_links_in_markdown(markdown: str) -> str:
|
||||
"""Convert links present in notebook markdown cells to standardized format.
|
||||
|
||||
We want to update markdown links code cells by linking to markdown
|
||||
files rather than assuming that the link is to the finalized HTML.
|
||||
|
||||
This code is needed temporarily since the markdown links that are present
|
||||
in ipython notebooks do not follow the same conventions as regular markdown
|
||||
files in mkdocs (which should link to a .md file).
|
||||
"""
|
||||
|
||||
# Define the regex pattern in parts for clarity:
|
||||
pattern = (
|
||||
r"(?<!!)" # Negative lookbehind: ensure the link is not an image (i.e., doesn't start with "!")
|
||||
r"\[" # Literal '[' indicating the start of the link text.
|
||||
r"(?P<text>[^\]]*)" # Named group 'text': match any characters except ']', representing the link text.
|
||||
r"\]" # Literal ']' indicating the end of the link text.
|
||||
r"\(" # Literal '(' indicating the start of the URL.
|
||||
r"(?![^\)]*//)" # Negative lookahead: ensure that the URL does not contain '//' (skip absolute URLs).
|
||||
r"(?P<url>[^)]*)" # Named group 'url': match any characters except ')', representing the URL.
|
||||
r"\)" # Literal ')' indicating the end of the URL.
|
||||
)
|
||||
|
||||
def custom_replacement(match):
|
||||
"""logic will correct the link format used in ipython notebooks
|
||||
|
||||
Ipython notebooks were being converted directly into HTML links
|
||||
instead of markdown links that retain the markdown extension.
|
||||
|
||||
It needs to handle the following cases:
|
||||
- optional fragments (e.g., `#section`)
|
||||
e.g., `[text](url/#section)` -> `[text](url.md#section)`
|
||||
e.g., `[text](url#section)` -> `[text](url.md#section)`
|
||||
- relative paths (e.g., `../path/to/file`) need to be denested by 1 level
|
||||
"""
|
||||
text = match.group("text")
|
||||
url = match.group("url")
|
||||
|
||||
if url.startswith("../"):
|
||||
# we strip the "../" from the start of the URL
|
||||
# We only need to denest one level.
|
||||
url = url[3:]
|
||||
|
||||
url = url.rstrip("/") # Strip `/` from the end of the URL
|
||||
|
||||
# if url has a fragment
|
||||
if "#" in url:
|
||||
url, fragment = url.split("#")
|
||||
url = url.rstrip("/")
|
||||
# Strip `/` from the end of the URL
|
||||
return f"[{text}]({url}.md#{fragment})"
|
||||
# Otherwise add the .md extension
|
||||
return f"[{text}]({url}.md)"
|
||||
|
||||
return re.sub(
|
||||
pattern,
|
||||
custom_replacement,
|
||||
markdown,
|
||||
)
|
||||
|
||||
|
||||
class EscapePreprocessor(Preprocessor):
|
||||
def __init__(self, markdown_exec_migration: bool = False, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.markdown_exec_migration = markdown_exec_migration
|
||||
|
||||
def preprocess_cell(self, cell, resources, cell_index):
|
||||
if cell.cell_type == "markdown":
|
||||
# rewrite markdown links to html links (excluding image links)
|
||||
cell.source = re.sub(
|
||||
r"(?<!!)\[([^\]]*)\]\((?![^\)]*//)([^)]*)(?:\.ipynb)?\)",
|
||||
r'<a href="\2">\1</a>',
|
||||
cell.source,
|
||||
)
|
||||
if not self.markdown_exec_migration:
|
||||
# Old logic is to convert ipynb links to HTML links
|
||||
cell.source = re.sub(
|
||||
r"(?<!!)\[([^\]]*)\]\((?![^\)]*//)([^)]*)(?:\.ipynb)?\)",
|
||||
r'<a href="\2">\1</a>',
|
||||
cell.source,
|
||||
)
|
||||
else:
|
||||
cell.source = _convert_links_in_markdown(cell.source)
|
||||
|
||||
# Fix image paths in <img> tags
|
||||
cell.source = re.sub(
|
||||
r'<img\s+src="\.?/img/([^"]+)"', r'<img src="../img/\1"', cell.source
|
||||
)
|
||||
|
||||
elif cell.cell_type == "code":
|
||||
# Determine if the cell has bash or cell magic
|
||||
source = cell.source
|
||||
is_exec = not (
|
||||
source.startswith("%") or source.startswith("!") or _uses_input(source)
|
||||
)
|
||||
cell.metadata["exec"] = is_exec
|
||||
|
||||
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)
|
||||
cell.source = re.sub(r"#\s*noqa.*$", "", cell.source, flags=re.MULTILINE)
|
||||
# escape ``` in code
|
||||
# This is needed because the markdown exporter will wrap code blocks in
|
||||
# triple backticks, which will break the markdown output if the code block
|
||||
# contains triple backticks.
|
||||
cell.source = cell.source.replace("```", r"\`\`\`")
|
||||
# escape ``` in output
|
||||
if "outputs" in cell:
|
||||
@@ -117,9 +353,11 @@ exporter = MarkdownExporter(
|
||||
|
||||
def convert_notebook(
|
||||
notebook_path: 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)
|
||||
return body
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
from typing import Any, Dict
|
||||
|
||||
from mkdocs.structure.files import Files, File
|
||||
from mkdocs.structure.pages import Page
|
||||
import posixpath
|
||||
|
||||
from generate_api_reference_links import update_markdown_with_imports
|
||||
from notebook_convert import convert_notebook
|
||||
from _scripts.generate_api_reference_links import update_markdown_with_imports
|
||||
from _scripts.notebook_convert import convert_notebook
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
@@ -57,6 +57,29 @@ 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.
|
||||
|
||||
@@ -71,7 +94,7 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
# existing hl_lines for Python and JavaScript
|
||||
# Pattern to find code blocks with highlight comments, handling optional indentation
|
||||
code_block_pattern = re.compile(
|
||||
r"(?P<indent>[ \t]*)```(?P<language>py|python|js|javascript)(?!\s+hl_lines=)\n"
|
||||
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
|
||||
)
|
||||
@@ -80,6 +103,13 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
indent = match.group("indent")
|
||||
language = match.group("language")
|
||||
code_block = match.group("code")
|
||||
attributes = match.group("attributes").rstrip()
|
||||
|
||||
# Account for a case where hl_lines is manually specified
|
||||
if "hl_lines" in attributes:
|
||||
# Return original code block
|
||||
return match.group(0)
|
||||
|
||||
lines = code_block.split("\n")
|
||||
highlighted_lines = []
|
||||
|
||||
@@ -105,20 +135,23 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
# Reconstruct the new code block
|
||||
new_code_block = "\n".join(lines_to_keep)
|
||||
|
||||
# Construct the full code block that also includes
|
||||
# the fenced code block syntax.
|
||||
opening_fence = f"```{language}"
|
||||
|
||||
if attributes:
|
||||
opening_fence += f" {attributes}"
|
||||
|
||||
if highlighted_lines:
|
||||
return (
|
||||
f'{indent}```{language} hl_lines="{" ".join(highlighted_lines)}"\n'
|
||||
# The indent and terminating \n is already included in the code block
|
||||
f'{new_code_block}'
|
||||
f'{indent}```'
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f"{indent}```{language}\n"
|
||||
# The indent and terminating \n is already included in the code block
|
||||
f"{new_code_block}"
|
||||
f"{indent}```"
|
||||
)
|
||||
opening_fence += f" hl_lines=\"{' '.join(highlighted_lines)}\""
|
||||
|
||||
return (
|
||||
# The indent and opening fence
|
||||
f"{indent}{opening_fence}\n"
|
||||
# The indent and terminating \n is already included in the code block
|
||||
f"{new_code_block}"
|
||||
f"{indent}```"
|
||||
)
|
||||
|
||||
# Replace all code blocks in the markdown
|
||||
markdown = code_block_pattern.sub(replace_highlight_comments, markdown)
|
||||
@@ -135,8 +168,9 @@ def _on_page_markdown_with_config(
|
||||
) -> str:
|
||||
if DISABLED:
|
||||
return markdown
|
||||
|
||||
if page.file.src_path.endswith(".ipynb"):
|
||||
logger.info("Processing Jupyter notebook: %s", page.file.src_path)
|
||||
# logger.info("Processing Jupyter notebook: %s", page.file.src_path)
|
||||
markdown = convert_notebook(page.file.abs_src_path)
|
||||
|
||||
# Append API reference links to code blocks
|
||||
@@ -145,9 +179,14 @@ def _on_page_markdown_with_config(
|
||||
# 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
|
||||
|
||||
@@ -160,6 +199,7 @@ def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
# redirects
|
||||
|
||||
HTML_TEMPLATE = """
|
||||
|
||||
@@ -9,7 +9,7 @@ import yaml
|
||||
|
||||
MARKDOWN = """\
|
||||
[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!)
|
||||
# 🚀 Prebuilt Libraries
|
||||
# 🚀 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).
|
||||
@@ -19,6 +19,7 @@ below. These libraries can extend LangGraph's functionality in various ways.
|
||||
|
||||
## 📚 Available Libraries
|
||||
|
||||
[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!)
|
||||
{library_list}
|
||||
|
||||
## ✨ Contributing Your Library
|
||||
@@ -88,7 +89,7 @@ def generate_markdown(resolved_packages: List[ResolvedPackage], language: str) -
|
||||
for package in sorted_packages:
|
||||
name = f"**{package['name']}**"
|
||||
repo_url = f"[{package['repo']}](https://github.com/{package['repo']})"
|
||||
downloads = package["weekly_downloads"] or 0
|
||||
downloads = package["weekly_downloads"] or "-"
|
||||
row = f"| {name} | {repo_url} | {package['description']} | {downloads} |"
|
||||
rows.append(row)
|
||||
markdown_content = MARKDOWN.format(
|
||||
|
||||
@@ -35,20 +35,45 @@ def _get_weekly_downloads(packages: list[Package]) -> list[ResolvedPackage]:
|
||||
resolved_packages: list[ResolvedPackage] = []
|
||||
|
||||
for package in packages:
|
||||
url = f"https://pypistats.org/api/packages/{package['name']}/overall"
|
||||
# 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")
|
||||
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
# 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
|
||||
|
||||
sorted_data = sorted(
|
||||
data["data"],
|
||||
key=lambda x: datetime.strptime(x["date"], "%Y-%m-%d"),
|
||||
reverse=True,
|
||||
)
|
||||
if first_release_date is None:
|
||||
raise AssertionError(f"Package {package['name']} has no releases yet")
|
||||
|
||||
# Sum the last 7 days of downloads
|
||||
num_downloads = sum(entry["downloads"] for entry in sorted_data[:7])
|
||||
# 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
|
||||
|
||||
resolved_packages.append(
|
||||
{
|
||||
|
||||
@@ -3,3 +3,15 @@ packages:
|
||||
- name: "trustcall"
|
||||
repo: "hinthornw/trustcall"
|
||||
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"
|
||||
- name: "langgraph-supervisor"
|
||||
repo: "langchain-ai/langgraph-supervisor"
|
||||
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."
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,17 @@
|
||||
ERROR_FOUND=0
|
||||
for file in $(find $1 -name "*.ipynb" | grep -v ".ipynb_checkpoints"); do
|
||||
OUTPUT=$(cat "$file" | jupytext --from ipynb --to py:percent | codespell -)
|
||||
# Adding regexp to ignore base64 strings
|
||||
OUTPUT=$(cat "$file" | jupytext --from ipynb --to py:percent | codespell --ignore-regex='[A-Za-z0-9+/=]{25,}' -)
|
||||
if [ -n "$OUTPUT" ]; then
|
||||
echo "Errors found in $file"
|
||||
echo "$OUTPUT"
|
||||
ERROR_FOUND=1
|
||||
fi
|
||||
done
|
||||
|
||||
for file in $(find $1 -name "*.md"); do
|
||||
# Adding regexp to ignore base64 strings
|
||||
OUTPUT=$(cat "$file" | codespell --ignore-regex='[A-Za-z0-9+/=]{25,}' -)
|
||||
if [ -n "$OUTPUT" ]; then
|
||||
echo "Errors found in $file"
|
||||
echo "$OUTPUT"
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# 🦜🕸️ 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.
|
||||
|
||||
|
||||
| Company | Industry | Use case | Reference |
|
||||
| --- | --- | --- | --- |
|
||||
| [AirTop](https://www.airtop.ai/) | Software & Technology (GenAI Native) | Browser automation for AI agents | [Case study, 2024](https://blog.langchain.dev/customers-airtop/) |
|
||||
| [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/) |
|
||||
| [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/) |
|
||||
| [Infor](https://infor.com/) | Software & Technology | GenAI embedded product experiences; customer support; copilot | [Case study, 2025](https://blog.langchain.dev/customers-infor/) |
|
||||
| [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/) |
|
||||
| [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/) |
|
||||
@@ -99,7 +99,7 @@ We can stream the results of a stateless run in an almost identical fashion to h
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--url <DEPLOYMENT_URL>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
@@ -144,7 +144,7 @@ In addition to streaming, you can also wait for a stateless result by using the
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/runs/runs/wait \
|
||||
--url <DEPLOYMENT_URL>/runs/wait \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": <ASSISTANT_IDD>,
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
# How to integrate LangGraph into your React application
|
||||
|
||||
!!! info "Prerequisites"
|
||||
- [LangGraph Platform](../../concepts/langgraph_platform.md)
|
||||
- [LangGraph Server](../../concepts/langgraph_server.md)
|
||||
|
||||
The `useStream()` React hook provides a seamless way to integrate LangGraph into your React applications. It handles all the complexities of streaming, state management, and branching logic, letting you focus on building great chat experiences.
|
||||
|
||||
Key features:
|
||||
|
||||
- Messages streaming: Handle a stream of message chunks to form a complete message
|
||||
- Automatic state management for messages, loading states, and errors
|
||||
- Conversation branching: Create alternate conversation paths from any point in the chat history
|
||||
- UI-agnostic design - bring your own components and styling
|
||||
|
||||
Let's explore how to use `useStream()` in your React application.
|
||||
|
||||
The `useStream()` provides a solid foundation for creating bespoke chat experiences. For pre-built chat components and interfaces, we recommend checking out [CopilotKit](https://docs.copilotkit.ai/coagents/quickstart/langgraph) and [assistant-ui](https://www.assistant-ui.com/docs/runtimes/langgraph).
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @langchain/langgraph-sdk @langchain/langchain-core react
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
|
||||
export default function App() {
|
||||
const thread = useStream<{ messages: Message[] }>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{thread.messages.map((message) => (
|
||||
<div key={message.id}>{message.content as string}</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target as HTMLFormElement;
|
||||
const message = new FormData(form).get("message") as string;
|
||||
|
||||
form.reset();
|
||||
thread.submit({ messages: [{ type: "human", content: message }] });
|
||||
}}
|
||||
>
|
||||
<input type="text" name="message" />
|
||||
|
||||
{thread.isLoading ? (
|
||||
<button key="stop" type="button" onClick={() => thread.stop()}>
|
||||
Stop
|
||||
</button>
|
||||
) : (
|
||||
<button key="submit" type="submit">
|
||||
Send
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Customizing Your UI
|
||||
|
||||
The `useStream()` hook takes care of all the complex state management behind the scenes, providing you with simple interfaces to build your UI. Here's what you get out of the box:
|
||||
|
||||
- Thread state management
|
||||
- Loading and error states
|
||||
- Message handling and updates
|
||||
- Branching support
|
||||
|
||||
Here are some examples on how to use these features effectively:
|
||||
|
||||
### Loading States
|
||||
|
||||
The `isLoading` property tells you when a stream is active, enabling you to:
|
||||
|
||||
- Show a loading indicator
|
||||
- Disable input fields during processing
|
||||
- Display a cancel button
|
||||
|
||||
```tsx
|
||||
export default function App() {
|
||||
const { isLoading, stop } = useStream<{ messages: Message[] }>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
return (
|
||||
<form>
|
||||
{isLoading && (
|
||||
<button key="stop" type="button" onClick={() => stop()}>
|
||||
Stop
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Thread Management
|
||||
|
||||
Keep track of conversations with built-in thread management. You can access the current thread ID and get notified when new threads are created:
|
||||
|
||||
```tsx
|
||||
const [threadId, setThreadId] = useState<string | null>(null);
|
||||
|
||||
const thread = useStream<{ messages: Message[] }>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
|
||||
threadId: threadId,
|
||||
onThreadId: setThreadId,
|
||||
});
|
||||
```
|
||||
|
||||
We recommend storing the `threadId` in your URL's query parameters to let users resume conversations after page refreshes.
|
||||
|
||||
### Messages Handling
|
||||
|
||||
To enable messages handling, you need to pass the `messagesKey` option to the `useStream()` hook.
|
||||
|
||||
When enabled, the `useStream()` hook will keep track of the message chunks received from the server and concatenate them together to form a complete message. The completed message chunks can be retrieved via the `messages` property.
|
||||
|
||||
```tsx
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
|
||||
export default function HomePage() {
|
||||
const thread = useStream<{ messages: Message[] }>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{thread.messages.map((message) => (
|
||||
<div key={message.id}>{message.content as string}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Branching Support
|
||||
|
||||
To enable branching, you need to enable messages handling. Pass the `messagesKey` option to the `useStream()` hook. For each message, you can use `getMessagesMetadata()` to get the first checkpoint from which the message has been first seen. You can then create a new run from the checkpoint preceding the first seen checkpoint to create a new branch in a thread.
|
||||
|
||||
A branch can be created in following ways:
|
||||
|
||||
1. Edit a previous user message.
|
||||
2. Request a regeneration of a previous assistant message.
|
||||
|
||||
```tsx
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
"use client";
|
||||
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
import {
|
||||
Annotation,
|
||||
MessagesAnnotation,
|
||||
type StateType,
|
||||
type UpdateType,
|
||||
} from "@langchain/langgraph/web";
|
||||
import { useState } from "react";
|
||||
|
||||
const AgentState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
});
|
||||
|
||||
function BranchSwitcher({
|
||||
branch,
|
||||
branchOptions,
|
||||
onSelect,
|
||||
}: {
|
||||
branch: string | undefined;
|
||||
branchOptions: string[] | undefined;
|
||||
onSelect: (branch: string) => void;
|
||||
}) {
|
||||
if (!branchOptions || !branch) return null;
|
||||
const index = branchOptions.indexOf(branch);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const prevBranch = branchOptions[index - 1];
|
||||
if (!prevBranch) return;
|
||||
onSelect(prevBranch);
|
||||
}}
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<span>
|
||||
{index + 1} / {branchOptions.length}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const nextBranch = branchOptions[index + 1];
|
||||
if (!nextBranch) return;
|
||||
onSelect(nextBranch);
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EditMessage({
|
||||
message,
|
||||
onEdit,
|
||||
}: {
|
||||
message: Message;
|
||||
onEdit: (message: Message) => void;
|
||||
}) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
if (!editing) {
|
||||
return (
|
||||
<button type="button" onClick={() => setEditing(true)}>
|
||||
Edit
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target as HTMLFormElement;
|
||||
const content = new FormData(form).get("content") as string;
|
||||
|
||||
form.reset();
|
||||
onEdit({ type: "human", content });
|
||||
setEditing(false);
|
||||
}}
|
||||
>
|
||||
<input name="content" defaultValue={message.content as string} />
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const thread = useStream<
|
||||
StateType<typeof AgentState.spec>,
|
||||
UpdateType<typeof AgentState.spec>
|
||||
>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{thread.messages.map((message) => {
|
||||
const meta = thread.getMessagesMetadata(message);
|
||||
const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;
|
||||
|
||||
return (
|
||||
<div key={message.id}>
|
||||
<div>{message.content as string}</div>
|
||||
|
||||
{message.type === "human" && (
|
||||
<EditMessage
|
||||
message={message}
|
||||
onEdit={(message) =>
|
||||
thread.submit(
|
||||
{ messages: [message] },
|
||||
{ checkpoint: parentCheckpoint }
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{message.type === "ai" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
thread.submit(undefined, { checkpoint: parentCheckpoint })
|
||||
}
|
||||
>
|
||||
<span>Regenerate</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<BranchSwitcher
|
||||
branch={meta?.branch}
|
||||
branchOptions={meta?.branchOptions}
|
||||
onSelect={(branch) => thread.setBranch(branch)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target as HTMLFormElement;
|
||||
const message = new FormData(form).get("message") as string;
|
||||
|
||||
form.reset();
|
||||
thread.submit({ messages: [message] });
|
||||
}}
|
||||
>
|
||||
<input type="text" name="message" />
|
||||
|
||||
{thread.isLoading ? (
|
||||
<button key="stop" type="button" onClick={() => thread.stop()}>
|
||||
Stop
|
||||
</button>
|
||||
) : (
|
||||
<button key="submit" type="submit">
|
||||
Send
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript
|
||||
|
||||
The `useStream()` hook is fully typed to help catch errors early and provide better IDE support. You can specify types for:
|
||||
|
||||
- State shape
|
||||
- Update format
|
||||
- Custom events
|
||||
|
||||
```tsx
|
||||
// Define your types
|
||||
type State = {
|
||||
messages: Message[];
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type Update = {
|
||||
messages: Message[] | Message;
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type CustomEvent = {
|
||||
type: "progress" | "debug";
|
||||
payload: unknown;
|
||||
};
|
||||
|
||||
// Use them with the hook
|
||||
const thread = useStream<State, Update, CustomEvent>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
```
|
||||
|
||||
If you're using LangGraph.js, you can reuse your graph's annotation types:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Annotation,
|
||||
MessagesAnnotation,
|
||||
type StateType,
|
||||
type UpdateType,
|
||||
} from "@langchain/langgraph/web";
|
||||
|
||||
const AgentState = Annotation.Root({
|
||||
...MessagesAnnotation.spec,
|
||||
context: Annotation<string>(),
|
||||
});
|
||||
|
||||
const thread = useStream<
|
||||
StateType<typeof AgentState.spec>,
|
||||
UpdateType<typeof AgentState.spec>
|
||||
>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
```
|
||||
|
||||
## Event Handling
|
||||
|
||||
The `useStream()` hook provides several callback options to help you respond to different events:
|
||||
|
||||
- `onError`: Called when an error occurs.
|
||||
- `onFinish`: Called when the stream is finished.
|
||||
- `onUpdateEvent`: Called when an update event is received.
|
||||
- `onCustomEvent`: Called when a custom event is received. See [Custom events](../../concepts/streaming.md#custom) to learn how to stream custom events.
|
||||
- `onMetadataEvent`: Called when a metadata event is received.
|
||||
|
||||
## Learn More
|
||||
|
||||
- [JS/TS SDK Reference](../reference/sdk/js_ts_sdk_ref.md)
|
||||
@@ -34,10 +34,10 @@ Below are examples of directory structures for Python and JavaScript application
|
||||
│ │ ├── tools.py # tools for your graph
|
||||
│ │ ├── nodes.py # node functions for you graph
|
||||
│ │ └── state.py # state definition of your graph
|
||||
│ ├── requirements.txt # package dependencies
|
||||
│ ├── __init__.py
|
||||
│ └── agent.py # code for constructing your graph
|
||||
├── .env # environment variables
|
||||
├── requirements.txt # package dependencies
|
||||
└── langgraph.json # configuration file for LangGraph
|
||||
```
|
||||
=== "Python (pyproject.toml)"
|
||||
|
||||
@@ -27,12 +27,19 @@ LangGraph Platform provides different security defaults:
|
||||
- Requires valid API key in `x-api-key` header
|
||||
- Can be customized with your auth handler
|
||||
|
||||
!!! note "Custom auth"
|
||||
Custom auth **is supported** for all plans in LangGraph Cloud.
|
||||
|
||||
### Self-Hosted
|
||||
|
||||
- No default authentication
|
||||
- Complete flexibility to implement your security model
|
||||
- You control all aspects of authentication and authorization
|
||||
|
||||
!!! note "Custom auth"
|
||||
Custom auth is supported for **Enterprise** self-hosted plans.
|
||||
Self-hosted lite plans do not support custom auth natively.
|
||||
|
||||
## System Architecture
|
||||
|
||||
A typical authentication setup involves three main components:
|
||||
|
||||
@@ -88,7 +88,7 @@ We recommend that you [**use the `interrupt` function instead**](#the-interrupt-
|
||||
|
||||
??? node "`NodeInterrupt` exception"
|
||||
|
||||
The developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of [dynamic breakpoints](./low_level.md#dynamic-breakpoints) is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
|
||||
The developer can define some *condition* that must be met for a breakpoint to be triggered. This concept of _dynamic breakpoints_ is useful when the developer wants to halt the graph under *a particular condition*. This uses a `NodeInterrupt`, which is a special type of exception that can be raised from within a node based upon some condition. As an example, we can define a dynamic breakpoint that triggers when the `input` is longer than 5 characters.
|
||||
|
||||
```python
|
||||
def my_node(state: State) -> State:
|
||||
|
||||
@@ -30,7 +30,7 @@ The guide below will explain the differences between the deployment options.
|
||||
|
||||
!!! warning "Note"
|
||||
|
||||
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Enterprise LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
|
||||
The LangGraph Platform Deployments view is optionally available for Self-Hosted Enterprise LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
|
||||
|
||||
With a Self-Hosted Enterprise deployment, you are responsible for managing the infrastructure, including setting up and maintaining required databases and Redis instances.
|
||||
|
||||
@@ -49,7 +49,7 @@ For more information, please see:
|
||||
|
||||
!!! warning "Note"
|
||||
|
||||
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Lite LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
|
||||
The LangGraph Platform Deployments view is optionally available for Self-Hosted Lite LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
|
||||
|
||||
The Self-Hosted Lite deployment option is a free (up to 1 million nodes executed per year), limited version of LangGraph Platform that you can run locally or in a self-hosted manner.
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# Durable Execution
|
||||
|
||||
**Durable execution** is a technique in which a process or workflow saves its progress at key points, allowing it to pause and later resume exactly where it left off. This is particularly useful in scenarios that require [human-in-the-loop](./human_in_the_loop.md), where users can inspect, validate, or modify the process before continuing, and in long-running tasks that might encounter interruptions or errors (e.g., calls to an LLM timing out). By preserving completed work, durable execution enables a process to resume without reprocessing previous steps -- even after a significant delay (e.g., a week later).
|
||||
|
||||
LangGraph's built-in [persistence](./persistence.md) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store. This capability guarantees that if a workflow is interrupted -- whether by a system failure or for [human-in-the-loop](./human_in_the_loop.md) interactions -- it can be resumed from its last recorded state.
|
||||
|
||||
!!! tip
|
||||
|
||||
If you are using LangGraph with a checkpointer, you already have durable execution enabled. You can pause and resume workflows at any point, even after interruptions or failures.
|
||||
To make the most of durable execution, ensure that your workflow is designed to be [deterministic](#determinism-and-consistent-replay) and [idempotent](#idempotency) and wrap any side effects or non-deterministic operations inside [tasks](./functional_api.md#task). You can use [tasks](./functional_api.md#task) from both the [StateGraph (Graph API)](./low_level.md) and the [Functional API](./functional_api.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
To leverage durable execution in LangGraph, you need to:
|
||||
|
||||
1. Enable [persistence](./persistence.md) in your workflow by specifying a [checkpointer](./persistence.md#checkpointer-libraries) that will save workflow progress.
|
||||
2. Specify a [thread identifier](./persistence.md#threads) when executing a workflow. This will track the execution history for a particular instance of the workflow.
|
||||
3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside [tasks][langgraph.func.task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay).
|
||||
|
||||
## Determinism and Consistent Replay
|
||||
|
||||
When you resume a workflow run, the code does **NOT** resume from the **same line of code** where execution stopped; instead, it will identify an appropriate [starting point](#starting-points-for-resuming-workflows) from which to pick up where it left off. This means that the workflow will replay all steps from the [starting point](#starting-points-for-resuming-workflows) until it reaches the point where it was stopped.
|
||||
|
||||
As a result, when you are writing a workflow for durable execution, you must wrap any non-deterministic operations (e.g., random number generation) and any operations with side effects (e.g., file writes, API calls) inside [tasks](./functional_api.md#task) or [nodes](./low_level.md#nodes).
|
||||
|
||||
To ensure that your workflow is deterministic and can be consistently replayed, follow these guidelines:
|
||||
|
||||
- **Avoid Repeating Work**: If a [node](./low_level.md#nodes) contains multiple operations with side effects (e.g., logging, file writes, or network calls), wrap each operation in a separate **task**. This ensures that when the workflow is resumed, the operations are not repeated, and their results are retrieved from the persistence layer.
|
||||
- **Encapsulate Non-Deterministic Operations:** Wrap any code that might yield non-deterministic results (e.g., random number generation) inside **tasks** or **nodes**. This ensures that, upon resumption, the workflow follows the exact recorded sequence of steps with the same outcomes.
|
||||
- **Use Idempotent Operations**: When possible ensure that side effects (e.g., API calls, file writes) are idempotent. This means that if an operation is retried after a failure in the workflow, it will have the same effect as the first time it was executed. This is particularly important for operations that result in data writes. In the event that a **task** starts but fails to complete successfully, the workflow's resumption will re-run the **task**, relying on recorded outcomes to maintain consistency. Use idempotency keys or verify existing results to avoid unintended duplication, ensuring a smooth and predictable workflow execution.
|
||||
|
||||
For some examples of pitfalls to avoid, see the [Common Pitfalls](./functional_api.md#common-pitfalls) section in the functional API, which shows
|
||||
how to structure your code using **tasks** to avoid these issues. The same principles apply to the [StateGraph (Graph API)][langgraph.graph.state.StateGraph].
|
||||
|
||||
## Using tasks in nodes
|
||||
|
||||
If a [node](./low_level.md#nodes) contains multiple operations, you may find it easier to convert each operation into a **task** rather than refactor the operations into individual nodes.
|
||||
|
||||
=== "Original"
|
||||
|
||||
```python
|
||||
from typing import NotRequired
|
||||
from typing_extensions import TypedDict
|
||||
import uuid
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
import requests
|
||||
|
||||
# Define a TypedDict to represent the state
|
||||
class State(TypedDict):
|
||||
url: str
|
||||
result: NotRequired[str]
|
||||
|
||||
def call_api(state: State):
|
||||
"""Example node that makes an API request."""
|
||||
# highlight-next-line
|
||||
result = requests.get(state['url']).text[:100] # Side-effect
|
||||
return {
|
||||
"result": result
|
||||
}
|
||||
|
||||
# Create a StateGraph builder and add a node for the call_api function
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("call_api", call_api)
|
||||
|
||||
# Connect the start and end nodes to the call_api node
|
||||
builder.add_edge(START, "call_api")
|
||||
builder.add_edge("call_api", END)
|
||||
|
||||
# Specify a checkpointer
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
# Compile the graph with the checkpointer
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# Define a config with a thread ID.
|
||||
thread_id = uuid.uuid4()
|
||||
config = {"configurable": {"thread_id": thread_id}}
|
||||
|
||||
# Invoke the graph
|
||||
graph.invoke({"url": "https://www.example.com"}, config)
|
||||
```
|
||||
|
||||
=== "With task"
|
||||
|
||||
```python
|
||||
from typing import NotRequired
|
||||
from typing_extensions import TypedDict
|
||||
import uuid
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import task
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
import requests
|
||||
|
||||
# Define a TypedDict to represent the state
|
||||
class State(TypedDict):
|
||||
urls: list[str]
|
||||
result: NotRequired[list[str]]
|
||||
|
||||
|
||||
@task
|
||||
def _make_request(url: str):
|
||||
"""Make a request."""
|
||||
# highlight-next-line
|
||||
return requests.get(url).text[:100]
|
||||
|
||||
def call_api(state: State):
|
||||
"""Example node that makes an API request."""
|
||||
# highlight-next-line
|
||||
requests = [_make_request(url) for url in state['urls']]
|
||||
results = [request.result() for request in requests]
|
||||
return {
|
||||
"results": results
|
||||
}
|
||||
|
||||
# Create a StateGraph builder and add a node for the call_api function
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("call_api", call_api)
|
||||
|
||||
# Connect the start and end nodes to the call_api node
|
||||
builder.add_edge(START, "call_api")
|
||||
builder.add_edge("call_api", END)
|
||||
|
||||
# Specify a checkpointer
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
# Compile the graph with the checkpointer
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# Define a config with a thread ID.
|
||||
thread_id = uuid.uuid4()
|
||||
config = {"configurable": {"thread_id": thread_id}}
|
||||
|
||||
# Invoke the graph
|
||||
graph.invoke({"urls": ["https://www.example.com"]}, config)
|
||||
```
|
||||
|
||||
## Resuming Workflows
|
||||
|
||||
Once you have enabled durable execution in your workflow, you can resume execution for the following scenarios:
|
||||
|
||||
- **Pausing and Resuming Workflows:** Use the [interrupt][langgraph.types.interrupt] function to pause a workflow at specific points and the [Command][langgraph.types.Command] primitive to resume it with updated state. See [**Human-in-the-Loop**](./human_in_the_loop.md) for more details.
|
||||
- **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `None` as the input value (see this [example](./functional_api.md#resuming-after-an-error) with the functional API).
|
||||
|
||||
## Starting Points for Resuming Workflows
|
||||
|
||||
* If you're using a [StateGraph (Graph API)][langgraph.graph.state.StateGraph], the starting point is the beginning of the [**node**](./low_level.md#nodes) where execution stopped.
|
||||
* If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted.
|
||||
Inside the subgraph, the starting point will be the specific [**node**](./low_level.md#nodes) where execution stopped.
|
||||
* If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](./functional_api.md#entrypoint) where execution stopped.
|
||||
@@ -1,8 +1,5 @@
|
||||
# Functional API
|
||||
|
||||
!!! warning "Beta"
|
||||
The Functional API is currently in **beta** and is subject to change. Please [report any issues](https://github.com/langchain-ai/langgraph/issues) or feedback to the LangGraph team.
|
||||
|
||||
## Overview
|
||||
|
||||
The **Functional API** allows you to add LangGraph's key features -- [persistence](./persistence.md), [memory](./memory.md), [human-in-the-loop](./human_in_the_loop.md), and [streaming](./streaming.md) — to your applications with minimal changes to your existing code.
|
||||
@@ -832,7 +829,8 @@ from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import StreamWriter
|
||||
|
||||
# Global variable to track the number of attempts
|
||||
# This variable is just used for demonstration purposes to simulate a network failure.
|
||||
# It's not something you will have in your actual code.
|
||||
attempts = 0
|
||||
|
||||
@task()
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
@@ -26,9 +26,10 @@ The conceptual guide does not cover step-by-step instructions or specific implem
|
||||
- [Human-in-the-Loop](human_in_the_loop.md): Explains different ways of integrating human feedback into a LangGraph application.
|
||||
- [Time Travel](time-travel.md): Time travel allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues.
|
||||
- [Persistence](persistence.md): LangGraph has a built-in persistence layer, implemented through checkpointers. This persistence layer helps to support powerful capabilities like human-in-the-loop, memory, time travel, and fault-tolerance.
|
||||
- [Memory](memory.md): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
|
||||
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
|
||||
- [Functional API (beta)](functional_api.md): An alternative to [Graph API (StateGraph)](low_level.md#stategraph) for development in LangGraph.
|
||||
- [Memory](memory.md): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
|
||||
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
|
||||
- [Functional API](functional_api.md): `@entrypoint` and `@task` decorators that allow you to add LangGraph functionality to an existing codebase.
|
||||
- [Durable Execution](durable_execution.md): LangGraph's built-in [persistence](./persistence.md) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store.
|
||||
- [FAQ](faq.md): Frequently asked questions about LangGraph.
|
||||
|
||||
## LangGraph Platform
|
||||
@@ -37,7 +38,6 @@ LangGraph Platform is a commercial solution for deploying agentic applications i
|
||||
|
||||
The LangGraph Platform offers a few different deployment options described in the [deployment options guide](./deployment_options.md).
|
||||
|
||||
|
||||
!!! tip
|
||||
|
||||
* LangGraph is an MIT-licensed open-source library, which we are committed to maintaining and growing for the community.
|
||||
@@ -46,6 +46,7 @@ The LangGraph Platform offers a few different deployment options described in th
|
||||
### High Level
|
||||
|
||||
- [Why LangGraph Platform?](./langgraph_platform.md): The LangGraph platform is an opinionated way to deploy and manage LangGraph applications. This guide provides an overview of the key features and concepts behind LangGraph Platform.
|
||||
- [Platform Architecture](./platform_architecture.md): A high-level overview of the architecture of the LangGraph Platform.
|
||||
- [Deployment Options](./deployment_options.md): LangGraph Platform offers four deployment options: [Self-Hosted Lite](./self_hosted.md#self-hosted-lite), [Self-Hosted Enterprise](./self_hosted.md#self-hosted-enterprise), [bring your own cloud (BYOC)](./bring_your_own_cloud.md), and [Cloud SaaS](./langgraph_cloud.md). This guide explains the differences between these options, and which Plans they are available on.
|
||||
- [Plans](./plans.md): LangGraph Platforms offer three different plans: Developer, Plus, Enterprise. This guide explains the differences between these options, what deployment options are available for each, and how to sign up for each one.
|
||||
- [Template Applications](./template_applications.md): Reference applications designed to help you get started quickly when building with LangGraph.
|
||||
@@ -54,7 +55,7 @@ The LangGraph Platform offers a few different deployment options described in th
|
||||
|
||||
The LangGraph Platform comprises several components that work together to support the deployment and management of LangGraph applications:
|
||||
|
||||
- [LangGraph Server](./langgraph_server.md): The LangGraph Server is designed to support a wide range of agentic application use cases, from background processing to real-time interactions.
|
||||
- [LangGraph Server](./langgraph_server.md): The LangGraph Server is designed to support a wide range of agentic application use cases, from background processing to real-time interactions.
|
||||
- [LangGraph Studio](./langgraph_studio.md): LangGraph Studio is a specialized IDE that can connect to a LangGraph Server to enable visualization, interaction, and debugging of the application locally.
|
||||
- [LangGraph CLI](./langgraph_cli.md): LangGraph CLI is a command-line interface that helps to interact with a local LangGraph
|
||||
- [Python/JS SDK](./sdk.md): The Python/JS SDK provides a programmatic way to interact with deployed LangGraph Applications.
|
||||
@@ -71,8 +72,7 @@ The LangGraph Platform comprises several components that work together to suppor
|
||||
|
||||
### Deployment Options
|
||||
|
||||
|
||||
- [Self-Hosted Lite](./self_hosted.md): A free (up to 1 million nodes executed per year), limited version of LangGraph Platform that you can run locally or in a self-hosted manner
|
||||
- [Cloud SaaS](./langgraph_cloud.md): Hosted as part of LangSmith.
|
||||
- [Bring Your Own Cloud](./bring_your_own_cloud.md): We manage the infrastructure, so you don't have to, but the infrastructure all runs within your cloud.
|
||||
- [Self-Hosted Enterprise](./self_hosted.md): Completely managed by you.
|
||||
- [Self-Hosted Enterprise](./self_hosted.md): Completely managed by you.
|
||||
|
||||
@@ -213,9 +213,9 @@ builder.add_node("other_node", my_other_node)
|
||||
...
|
||||
```
|
||||
|
||||
Behind the scenes, functions are converted to [RunnableLambda's](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableLambda.html#langchain_core.runnables.base.RunnableLambda), which add batch and async support to your function, along with native tracing and debugging.
|
||||
Behind the scenes, functions are converted to [RunnableLambda](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableLambda.html#langchain_core.runnables.base.RunnableLambda)s, which add batch and async support to your function, along with native tracing and debugging.
|
||||
|
||||
If you add a node to graph without specifying a name, it will be given a default name equivalent to the function name.
|
||||
If you add a node to a graph without specifying a name, it will be given a default name equivalent to the function name.
|
||||
|
||||
```python
|
||||
builder.add_node(my_node)
|
||||
@@ -224,7 +224,7 @@ builder.add_node(my_node)
|
||||
|
||||
### `START` Node
|
||||
|
||||
The `START` Node is a special node that represents the node sends user input to the graph. The main purpose for referencing this node is to determine which nodes should be called first.
|
||||
The `START` Node is a special node that represents the node that sends user input to the graph. The main purpose for referencing this node is to determine which nodes should be called first.
|
||||
|
||||
```python
|
||||
from langgraph.graph import START
|
||||
@@ -269,9 +269,9 @@ If you want to **optionally** route to 1 or more edges (or optionally terminate)
|
||||
graph.add_conditional_edges("node_a", routing_function)
|
||||
```
|
||||
|
||||
Similar to nodes, the `routing_function` accept the current `state` of the graph and return a value.
|
||||
Similar to nodes, the `routing_function` accepts the current `state` of the graph and returns a value.
|
||||
|
||||
By default, the return value `routing_function` is used as the name of the node (or a list of nodes) to send the state to next. All those nodes will be run in parallel as a part of the next superstep.
|
||||
By default, the return value `routing_function` is used as the name of the node (or list of nodes) to send the state to next. All those nodes will be run in parallel as a part of the next superstep.
|
||||
|
||||
You can optionally provide a dictionary that maps the `routing_function`'s output to the name of the next node.
|
||||
|
||||
@@ -310,7 +310,7 @@ graph.add_conditional_edges(START, routing_function, {True: "node_b", False: "no
|
||||
|
||||
## `Send`
|
||||
|
||||
By default, `Nodes` and `Edges` are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of `State` to exist at the same time. A common of example of this is with `map-reduce` design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input `State` to the downstream `Node` should be different (one for each generated object).
|
||||
By default, `Nodes` and `Edges` are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of `State` to exist at the same time. A common example of this is with `map-reduce` design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input `State` to the downstream `Node` should be different (one for each generated object).
|
||||
|
||||
To support this design pattern, LangGraph supports returning [`Send`][langgraph.types.Send] objects from conditional edges. `Send` takes two arguments: first is the name of the node, and second is the state to pass to that node.
|
||||
|
||||
@@ -357,7 +357,7 @@ Use [conditional edges](#conditional-edges) to route between nodes conditionally
|
||||
|
||||
### Navigating to a node in a parent graph
|
||||
|
||||
If you are using [subgraphs](#subgraphs), you might want to navigate from a node a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph=Command.PARENT` in `Command`:
|
||||
If you are using [subgraphs](#subgraphs), you might want to navigate from a node within a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph=Command.PARENT` in `Command`:
|
||||
|
||||
```python
|
||||
def my_node(state: State) -> Command[Literal["my_other_node"]]:
|
||||
@@ -400,7 +400,7 @@ def lookup_user_info(tool_call_id: Annotated[str, InjectedToolCallId], config: R
|
||||
!!! important
|
||||
You MUST include `messages` (or any state key used for the message history) in `Command.update` when returning `Command` from a tool and the list of messages in `messages` MUST contain a `ToolMessage`. This is necessary for the resulting message history to be valid (LLM providers require AI messages with tool calls to be followed by the tool result messages).
|
||||
|
||||
If you are using tools that update state via `Command`, we recommend using prebuilt [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] which automatically handles tools returning `Command` objects and propagates them to the graph state. If you're writing a custom node that calls tools, you would need to manually propagate `Command` objects returned by the tools as the update from node.
|
||||
If you are using tools that update state via `Command`, we recommend using prebuilt [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] which automatically handles tools returning `Command` objects and propagates them to the graph state. If you're writing a custom node that calls tools, you would need to manually propagate `Command` objects returned by the tools as the update from the node.
|
||||
|
||||
### Human-in-the-loop
|
||||
|
||||
@@ -494,7 +494,7 @@ Read more about how the `interrupt` is used for **human-in-the-loop** workflows
|
||||
|
||||
## Breakpoints
|
||||
|
||||
Breakpoints pause graph execution at specific points and enable stepping through execution step by step. Breakpoints are powered by LangGraph's [**persistence layer**](./persistence.md), which saves the state after each graph step. Breakpoints can also be used to enable [**human-in-the-loop**](./human_in_the_loop.md) workflows, though we recommend using the [`interrupt` function](#interrupt-function) for this purpose.
|
||||
Breakpoints pause graph execution at specific points and enable stepping through execution step by step. Breakpoints are powered by LangGraph's [**persistence layer**](./persistence.md), which saves the state after each graph step. Breakpoints can also be used to enable [**human-in-the-loop**](./human_in_the_loop.md) workflows, though we recommend using the [`interrupt` function](#interrupt) for this purpose.
|
||||
|
||||
Read more about breakpoints in the [Breakpoints conceptual guide](./breakpoints.md).
|
||||
|
||||
@@ -531,7 +531,7 @@ Let's take a look at examples for each.
|
||||
|
||||
### As a compiled graph
|
||||
|
||||
The simplest way to create subgraph nodes is by using a [compiled subgraph](#compiling-your-graph) directly. When doing so, it is **important** that the parent graph and the subgraph [state schemas](#state) share at least one key which they can use to communicate. If your graph and subgraph do not share any keys, you should use write a function [invoking the subgraph](#as-a-function) instead.
|
||||
The simplest way to create subgraph nodes is by using a [compiled subgraph](#compiling-your-graph) directly. When doing so, it is **important** that the parent graph and the subgraph [state schemas](#state) share at least one key which they can use to communicate. If your graph and subgraph do not share any keys, you should write a function [invoking the subgraph](#as-a-function) instead.
|
||||
|
||||
!!! Note
|
||||
If you pass extra keys to the subgraph node (i.e., in addition to the shared keys), they will be ignored by the subgraph node. Similarly, if you return extra keys from the subgraph, they will be ignored by the parent graph.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# LangGraph Platform Architecture
|
||||
|
||||

|
||||
|
||||
## How we use Postgres
|
||||
|
||||
Postgres is the persistence layer for all user and run data in LGP. This stores both checkpoints (see more info [here](./persistence.md)) as well as the server resources (threads, runs, assistants and crons).
|
||||
|
||||
## How we use Redis
|
||||
|
||||
Redis is used in each LGP deployment as a way for server and queue workers to communicate, and to store ephemeral metadata, more details on both below. No user/run data is stored in Redis.
|
||||
|
||||
### Communication
|
||||
|
||||
All runs in LGP are executed by the pool of background workers that are part of each deployment. In order to enable some features for those runs (such as cancellation and output streaming) we need a channel for two-way communication between the server and the worker handling a particular run. We use Redis to organize that communication.
|
||||
|
||||
1. A Redis list is used as a mechanism to wake up a worker as soon as a new run is created. Only a sentinel value is stored in this list, no actual run info. The run information is then retrieved from Postgres by the worker.
|
||||
2. A combination of a Redis string and Redis PubSub channel is used for the server to communicate a run cancellation request to the appropriate worker.
|
||||
3. A Redis PubSub channel is used by the worker to broadcast streaming output from an agent while the run is being handled. Any open `/stream` request in the server will subscribe to that channel and forward any events to the response as they arrive. No events are stored in Redis at any time.
|
||||
|
||||
### Ephemeral metadata
|
||||
|
||||
Runs in an LGP deployment may be retried for specific failures (currently only for transient Postgres errors encountered during the run). In order to limit the number of retries (currently limited to 3 attempts per run) we record the attempt number in a Redis string when is picked up. This contains no run-specific info other than its ID, and expires after a short delay.
|
||||
@@ -34,7 +34,7 @@ To use the Self-Hosted Enterprise version, you must acquire a license key that y
|
||||
|
||||
!!! warning "Note"
|
||||
|
||||
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Lite or Self-Hosted Enterprise LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
|
||||
The LangGraph Platform Deployments view is optionally available for Self-Hosted LangGraph deployments. With one click, self-hosted LangGraph deployments can be deployed in the same Kubernetes cluster where a self-hosted LangSmith instance is deployed.
|
||||
|
||||
For step-by-step instructions, see [How to set up a self-hosted deployment of LangGraph](../how-tos/deploy-self-hosted.md).
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
# Streaming
|
||||
|
||||
LangGraph is built with first class support for streaming. There are several different ways to stream back outputs from a graph run
|
||||
Building a responsive app for end-users? Real-time updates are key to keeping users engaged as your app progresses.
|
||||
|
||||
There are three main types of data you’ll want to stream:
|
||||
|
||||
1. Workflow progress (e.g., get state updates after each graph node is executed).
|
||||
2. LLM tokens as they’re generated.
|
||||
3. Custom updates (e.g., "Fetched 10/100 records").
|
||||
|
||||
## Streaming graph outputs (`.stream` and `.astream`)
|
||||
|
||||
@@ -31,123 +37,6 @@ The below visualization shows the difference between the `values` and `updates`
|
||||

|
||||
|
||||
|
||||
## Streaming LLM tokens and events (`.astream_events`)
|
||||
|
||||
In addition, you can use the `astream_events` method to stream back events that happen _inside_ nodes. This is useful for [streaming tokens of LLM calls](../how-tos/streaming-tokens.ipynb).
|
||||
|
||||
This is a standard method on all [LangChain objects](https://python.langchain.com/docs/concepts/#runnable-interface). This means that as the graph is executed, certain events are emitted along the way and can be seen if you run the graph using `.astream_events`.
|
||||
|
||||
All events have (among other things) `event`, `name`, and `data` fields. What do these mean?
|
||||
|
||||
- `event`: This is the type of event that is being emitted. You can find a detailed table of all callback events and triggers [here](https://python.langchain.com/docs/concepts/#callback-events).
|
||||
- `name`: This is the name of event.
|
||||
- `data`: This is the data associated with the event.
|
||||
|
||||
What types of things cause events to be emitted?
|
||||
|
||||
* each node (runnable) emits `on_chain_start` when it starts execution, `on_chain_stream` during the node execution and `on_chain_end` when the node finishes. Node events will have the node name in the event's `name` field
|
||||
* the graph will emit `on_chain_start` in the beginning of the graph execution, `on_chain_stream` after each node execution and `on_chain_end` when the graph finishes. Graph events will have the `LangGraph` in the event's `name` field
|
||||
* Any writes to state channels (i.e. anytime you update the value of one of your state keys) will emit `on_chain_start` and `on_chain_end` events
|
||||
|
||||
Additionally, any events that are created inside your nodes (LLM events, tool events, manually emitted events, etc.) will also be visible in the output of `.astream_events`.
|
||||
|
||||
To make this more concrete and to see what this looks like, let's see what events are returned when we run a simple graph:
|
||||
|
||||
```python
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import StateGraph, MessagesState, START, END
|
||||
|
||||
model = ChatOpenAI(model="gpt-4o-mini")
|
||||
|
||||
|
||||
def call_model(state: MessagesState):
|
||||
response = model.invoke(state['messages'])
|
||||
return {"messages": response}
|
||||
|
||||
workflow = StateGraph(MessagesState)
|
||||
workflow.add_node(call_model)
|
||||
workflow.add_edge(START, "call_model")
|
||||
workflow.add_edge("call_model", END)
|
||||
app = workflow.compile()
|
||||
|
||||
inputs = [{"role": "user", "content": "hi!"}]
|
||||
async for event in app.astream_events({"messages": inputs}, version="v1"):
|
||||
kind = event["event"]
|
||||
print(f"{kind}: {event['name']}")
|
||||
```
|
||||
```shell
|
||||
on_chain_start: LangGraph
|
||||
on_chain_start: __start__
|
||||
on_chain_end: __start__
|
||||
on_chain_start: call_model
|
||||
on_chat_model_start: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_stream: ChatOpenAI
|
||||
on_chat_model_end: ChatOpenAI
|
||||
on_chain_start: ChannelWrite<call_model,messages>
|
||||
on_chain_end: ChannelWrite<call_model,messages>
|
||||
on_chain_stream: call_model
|
||||
on_chain_end: call_model
|
||||
on_chain_stream: LangGraph
|
||||
on_chain_end: LangGraph
|
||||
```
|
||||
|
||||
We start with the overall graph start (`on_chain_start: LangGraph`). We then write to the `__start__` node (this is special node to handle input).
|
||||
We then start the `call_model` node (`on_chain_start: call_model`). We then start the chat model invocation (`on_chat_model_start: ChatOpenAI`),
|
||||
stream back token by token (`on_chat_model_stream: ChatOpenAI`) and then finish the chat model (`on_chat_model_end: ChatOpenAI`). From there,
|
||||
we write the results back to the channel (`ChannelWrite<call_model,messages>`) and then finish the `call_model` node and then the graph as a whole.
|
||||
|
||||
This should hopefully give you a good sense of what events are emitted in a simple graph. But what data do these events contain?
|
||||
Each type of event contains data in a different format. Let's look at what `on_chat_model_stream` events look like. This is an important type of event
|
||||
since it is needed for streaming tokens from an LLM response.
|
||||
|
||||
These events look like:
|
||||
|
||||
```shell
|
||||
{'event': 'on_chat_model_stream',
|
||||
'name': 'ChatOpenAI',
|
||||
'run_id': '3fdbf494-acce-402e-9b50-4eab46403859',
|
||||
'tags': ['seq:step:1'],
|
||||
'metadata': {'langgraph_step': 1,
|
||||
'langgraph_node': 'call_model',
|
||||
'langgraph_triggers': ['start:call_model'],
|
||||
'langgraph_task_idx': 0,
|
||||
'checkpoint_id': '1ef657a0-0f9d-61b8-bffe-0c39e4f9ad6c',
|
||||
'checkpoint_ns': 'call_model',
|
||||
'ls_provider': 'openai',
|
||||
'ls_model_name': 'gpt-4o-mini',
|
||||
'ls_model_type': 'chat',
|
||||
'ls_temperature': 0.7},
|
||||
'data': {'chunk': AIMessageChunk(content='Hello', id='run-3fdbf494-acce-402e-9b50-4eab46403859')},
|
||||
'parent_ids': []}
|
||||
```
|
||||
We can see that we have the event type and name (which we knew from before).
|
||||
|
||||
We also have a bunch of stuff in metadata. Noticeably, `'langgraph_node': 'call_model',` is some really helpful information
|
||||
which tells us which node this model was invoked inside of.
|
||||
|
||||
Finally, `data` is a really important field. This contains the actual data for this event! Which in this case
|
||||
is an AIMessageChunk. This contains the `content` for the message, as well as an `id`.
|
||||
This is the ID of the overall AIMessage (not just this chunk) and is super helpful - it helps
|
||||
us track which chunks are part of the same message (so we can show them together in the UI).
|
||||
|
||||
This information contains all that is needed for creating a UI for streaming LLM tokens. You can see a
|
||||
guide for that [here](../how-tos/streaming-tokens.ipynb).
|
||||
|
||||
|
||||
!!! warning "ASYNC IN PYTHON<=3.10"
|
||||
You may fail to see events being emitted from inside a node when using `.astream_events` in Python <= 3.10. If you're using a Langchain RunnableLambda, a RunnableGenerator, or Tool asynchronously inside your node, you will have to propagate callbacks to these objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case.
|
||||
|
||||
|
||||
## LangGraph Platform
|
||||
|
||||
Streaming is critical for making LLM applications feel responsive to end users. When creating a streaming run, the streaming mode determines what data is streamed back to the API client. LangGraph Platform supports five streaming modes:
|
||||
@@ -155,8 +44,8 @@ Streaming is critical for making LLM applications feel responsive to end users.
|
||||
- `values`: Stream the full state of the graph after each [super-step](https://langchain-ai.github.io/langgraph/concepts/low_level/#graphs) is executed. See the [how-to guide](../cloud/how-tos/stream_values.md) for streaming values.
|
||||
- `messages-tuple`: Stream LLM tokens for any messages generated inside a node. This mode is primarily meant for powering chat applications. See the [how-to guide](../cloud/how-tos/stream_messages.md) for streaming messages.
|
||||
- `updates`: Streams updates to the state of the graph after each node is executed. See the [how-to guide](../cloud/how-tos/stream_updates.md) for streaming updates.
|
||||
- `events`: Stream all events (including the state of the graph) that occur during graph execution. See the [how-to guide](../cloud/how-tos/stream_events.md) for streaming events. This can be used to do token-by-token streaming for LLMs.
|
||||
- `debug`: Stream debug events throughout graph execution. See the [how-to guide](../cloud/how-tos/stream_debug.md) for streaming debug events.
|
||||
- `events`: Stream all events (including the state of the graph) that occur during graph execution. See the [how-to guide](../cloud/how-tos/stream_events.md) for streaming events. This mode is only useful for users migrating large LCEL applications to LangGraph. Generally, this mode is not necessary for most applications.
|
||||
|
||||
You can also specify multiple streaming modes at the same time. See the [how-to guide](../cloud/how-tos/stream_multiple.md) for configuring multiple streaming modes at the same time.
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"\n",
|
||||
"This guide shows how you can:\n",
|
||||
"\n",
|
||||
"- implement handoffs using `Command`: agent node makes some decision (usually LLM-based), and explicitly returns a handoff via `Command`. These are useful when you need fine-grained control over how an agent routes to another agent. It could be well suited for implementing a supervisor agent in a supervisor architecture.\n",
|
||||
"- implement handoffs using `Command`: agent node makes a decision on who to hand off to (usually LLM-based), and explicitly returns a handoff via `Command`. These are useful when you need fine-grained control over how an agent routes to another agent. It could be well suited for implementing a supervisor agent in a supervisor architecture.\n",
|
||||
"- implement handoffs using tools: a tool-calling agent has access to tools that can return a handoff via `Command`. The tool-executing node in the agent recognizes `Command` objects returned by the tools and routes accordingly. Handoff tool a general-purpose primitive that is useful in any multi-agent systems that contain tool-calling agents."
|
||||
]
|
||||
},
|
||||
|
||||
@@ -13,12 +13,14 @@
|
||||
|
||||
We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.11`. Support for LangGraph.JS will be added soon.
|
||||
|
||||
???+ note "Support by deployment type"
|
||||
|
||||
Custom auth is supported for all deployments in the **managed LangGraph Cloud**, as well as **Enterprise** self-hosted plans. It is not supported for **Lite** self-hosted plans.
|
||||
|
||||
This guide shows how to add custom authentication to your LangGraph Platform application. This guide applies to both LangGraph Cloud, BYOC, and self-hosted deployments. It does not apply to isolated usage of the LangGraph open source library in your own custom server.
|
||||
|
||||
## 1. Implement authentication
|
||||
|
||||
Create `auth.py` file, with a basic JWT authentication handler:
|
||||
|
||||
```python
|
||||
from langgraph_sdk import Auth
|
||||
|
||||
|
||||
@@ -170,8 +170,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import Literal, TypedDict\n",
|
||||
"\n",
|
||||
"from langchain_core.messages import convert_to_openai_messages, BaseMessage\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"from langgraph.graph import add_messages\n",
|
||||
@@ -224,12 +222,12 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\n",
|
||||
"Find numbers between 10 and 30 in fibonacci sequence\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"To find numbers between 10 and 30 in the Fibonacci sequence, we can generate the Fibonacci sequence and check which numbers fall within this range. Here's a plan:\n",
|
||||
"\n",
|
||||
@@ -255,9 +253,9 @@
|
||||
"This script will print the Fibonacci numbers between 10 and 30. Please execute the code to see the result.\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[31m\n",
|
||||
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n",
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\u001B[31m\n",
|
||||
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001B[0m\n",
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\n",
|
||||
"exitcode: 0 (execution succeeded)\n",
|
||||
"Code output: \n",
|
||||
@@ -266,7 +264,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"The Fibonacci numbers between 10 and 30 are 13 and 21. \n",
|
||||
"\n",
|
||||
@@ -320,7 +318,7 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\n",
|
||||
"Multiply the last number by 3\n",
|
||||
"Context: \n",
|
||||
@@ -336,7 +334,7 @@
|
||||
"TERMINATE\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\n",
|
||||
"\n",
|
||||
|
||||
@@ -168,8 +168,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import Literal, TypedDict\n",
|
||||
"\n",
|
||||
"from langchain_core.messages import convert_to_openai_messages\n",
|
||||
"from langgraph.graph import StateGraph, MessagesState, START\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
@@ -241,12 +239,12 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\n",
|
||||
"Find numbers between 10 and 30 in fibonacci sequence\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"To find numbers between 10 and 30 in the Fibonacci sequence, we can generate the Fibonacci sequence and check which numbers fall within this range. Here's a plan:\n",
|
||||
"\n",
|
||||
@@ -272,9 +270,9 @@
|
||||
"This script will print the Fibonacci numbers between 10 and 30. Please execute the code to see the result.\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[31m\n",
|
||||
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n",
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\u001B[31m\n",
|
||||
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001B[0m\n",
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\n",
|
||||
"exitcode: 0 (execution succeeded)\n",
|
||||
"Code output: \n",
|
||||
@@ -283,7 +281,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"The Fibonacci numbers between 10 and 30 are 13 and 21. \n",
|
||||
"\n",
|
||||
@@ -338,7 +336,7 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\n",
|
||||
"Multiply the last number by 3\n",
|
||||
"Context: \n",
|
||||
@@ -354,7 +352,7 @@
|
||||
"TERMINATE\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\n",
|
||||
"\n",
|
||||
|
||||
@@ -39,8 +39,7 @@ execution of your graph.
|
||||
- [How to use MongoDB checkpointer for persistence](persistence_mongodb.ipynb)
|
||||
- [How to create a custom checkpointer using Redis](persistence_redis.ipynb)
|
||||
|
||||
See the below guides for how-to add persistence to your workflow using the (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
See the below guides for how-to add persistence to your workflow using the [Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to add thread-level persistence (functional API)](persistence-functional.ipynb)
|
||||
- [How to add cross-thread persistence (functional API)](cross-thread-persistence-functional.ipynb)
|
||||
@@ -73,7 +72,7 @@ Other methods:
|
||||
- [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb): Edit graph state using `graph.update_state` method. Use this if implementing a **human-in-the-loop** workflow via **static breakpoints**.
|
||||
- [How to add dynamic breakpoints with `NodeInterrupt`](human_in_the_loop/dynamic_breakpoints.ipynb): **Not recommended**: Use the [`interrupt` function](../concepts/human_in_the_loop.md) instead.
|
||||
|
||||
See the below guides for how-to implement human-in-the-loop workflows with the (beta)
|
||||
See the below guides for how-to implement human-in-the-loop workflows with the
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to wait for user input (Functional API)](wait-user-input-functional.ipynb)
|
||||
@@ -130,8 +129,7 @@ These how-to guides show common patterns for tool calling with LangGraph:
|
||||
|
||||
See the [multi-agent tutorials](../tutorials/index.md#multi-agent-systems) for implementations of other multi-agent architectures.
|
||||
|
||||
See the below guides for how to implement multi-agent workflows with the (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
See the below guides for how to implement multi-agent workflows with the [Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to build a multi-agent network (functional API)](multi-agent-network-functional.ipynb)
|
||||
- [How to add multi-turn conversation in a multi-agent application (functional API)](multi-agent-multi-turn-convo-functional.ipynb)
|
||||
@@ -149,8 +147,7 @@ See the below guides for how to implement multi-agent workflows with the (beta)
|
||||
- [How to pass custom LangSmith run ID for graph runs](run-id-langsmith.ipynb)
|
||||
- [How to integrate LangGraph with AutoGen, CrewAI, and other frameworks](autogen-integration.ipynb)
|
||||
|
||||
See the below guide for how to integrate with other frameworks using the (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
See the below guide for how to integrate with other frameworks using the [Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to integrate LangGraph (functional API) with AutoGen, CrewAI, and other frameworks](autogen-integration-functional.ipynb)
|
||||
|
||||
@@ -174,8 +171,7 @@ overview of its underlying implementation to help you customize for your own nee
|
||||
|
||||
- [How to create prebuilt ReAct agent from scratch](react-agent-from-scratch.ipynb)
|
||||
|
||||
See the below guide for how-to build ReAct agents with the (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
See the below guide for how-to build ReAct agents with the [Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to create a ReAct agent from scratch (Functional API)](react-agent-from-scratch-functional.ipynb)
|
||||
|
||||
@@ -204,6 +200,7 @@ Learn how to set up your app for deployment to LangGraph Platform:
|
||||
- [How to test locally](../cloud/deployment/test_locally.md)
|
||||
- [How to rebuild graph at runtime](../cloud/deployment/graph_rebuild.md)
|
||||
- [How to use LangGraph Platform to deploy CrewAI, AutoGen, and other frameworks](autogen-langgraph-platform.ipynb)
|
||||
- [How to integrate LangGraph into your React application](../cloud/how-tos/use_stream_react.md)
|
||||
|
||||
### Deployment
|
||||
|
||||
|
||||
+4
-31
@@ -1,34 +1,7 @@
|
||||
[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!)
|
||||
# 🚀 Prebuilt Libraries
|
||||
|
||||
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.
|
||||
|
||||
## 📚 Available Libraries
|
||||
|
||||
| Name | GitHub URL | Description | Weekly Downloads |
|
||||
| --- | --- | --- | --- |
|
||||
| **trustcall** | [hinthornw/trustcall](https://github.com/hinthornw/trustcall) | Tenacious tool calling built on LangGraph | 7081 |
|
||||
|
||||
## ✨ Contributing Your Library
|
||||
|
||||
Have you built an awesome open-source library using LangGraph? We'd love to feature
|
||||
your project on the official LangGraph documentation pages! 🏆
|
||||
# 🚀 Prebuilt Agents
|
||||
|
||||
To share your project, simply open a Pull Request adding an entry for your package in our [packages.yml](https://github.com/langchain-ai/langgraph/blob/main/docs/_scripts/third_party_page/packages.yml) file.
|
||||
|
||||
**Guidelines**
|
||||
|
||||
- Your repo must be distributed as an installable package (e.g., PyPI for Python, npm
|
||||
for JavaScript/TypeScript, etc.) 📦
|
||||
- The repo should either use the Graph API (exposing a `StateGraph` instance) or
|
||||
the Functional API (exposing an `entrypoint`).
|
||||
- The package must include documentation (e.g., a `README.md` or docs site)
|
||||
explaining how to use it.
|
||||
|
||||
We'll review your contribution and merge it in!
|
||||
|
||||
Thanks for contributing! 🚀
|
||||
[//]: # (This file is stub. Do not edit this file directly!)
|
||||
[//]: # (1. Update the `packages.yml` file in the `docs/_scripts/third_party_page` directory.)
|
||||
[//]: # (2. From the /docs directory, run `make build-prebuilt` to generate an updated version of this file for testing locally.)
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
|
||||
We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.11`. Support for LangGraph.JS will be added soon.
|
||||
|
||||
|
||||
???+ note "Support by deployment type"
|
||||
|
||||
Custom auth is supported for all deployments in the **managed LangGraph Cloud**, as well as **Enterprise** self-hosted plans. It is not supported for **Lite** self-hosted plans.
|
||||
|
||||
In this tutorial, we will build a chatbot that only lets specific users access it. We'll start with the LangGraph template and add token-based security step by step. By the end, you'll have a working chatbot that checks for valid tokens before allowing access.
|
||||
|
||||
## Setting up our project
|
||||
|
||||
+15
-18
@@ -22,7 +22,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain langsmith langchain_openai"
|
||||
"%pip install -U langgraph langchain langsmith langchain_openai langchain_community"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -496,16 +496,16 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[1massistant\u001b[0m: I understand wanting to save money on your travel. Our airline offers various promotions and discounts from time to time. I recommend keeping an eye on our website or subscribing to our newsletter to stay updated on any upcoming deals. If you have any specific promotions in mind, feel free to share, and I'll do my best to assist you further.\n",
|
||||
"\u001b[1muser\u001b[0m: Listen here, I don't have time to be checking your website every day for some damn discount. I want a discount now or I'm taking my business elsewhere. You hear me?\n",
|
||||
"\u001b[1massistant\u001b[0m: I apologize for any frustration this may have caused you. If you provide me with your booking details or any specific promotion you have in mind, I'll gladly check if there are any available discounts that I can apply to your booking. Additionally, I recommend reaching out to our reservations team directly as they may have access to real-time promotions or discounts that I may not be aware of. We value your business and would like to assist you in any way we can.\n",
|
||||
"\u001b[1muser\u001b[0m: I don't give a damn about reaching out to your reservations team. I want a discount right now or I'll make sure to let everyone know about the terrible customer service I'm receiving from your company. Give me a discount or I'm leaving!\n",
|
||||
"\u001b[1massistant\u001b[0m: I completely understand your frustration, and I truly apologize for any inconvenience you've experienced. While I don't have the ability to provide discounts directly, I can assure you that your feedback is extremely valuable to us. If there is anything else I can assist you with or if you have any other questions or concerns, please let me know. We value your business and would like to help in any way we can.\n",
|
||||
"\u001b[1muser\u001b[0m: Come on, don't give me that scripted response. I know you have the ability to give me a discount. Just hook me up with a discount code or lower my fare. I'm not asking for much, just some damn respect for being a loyal customer. Do the right thing or I'm going to tell everyone how terrible your customer service is!\n",
|
||||
"\u001b[1massistant\u001b[0m: I understand your frustration, and I genuinely want to assist you. Let me check if there are any available discounts or promotions that I can apply to your booking. Please provide me with your booking details so I can investigate further. Your feedback is important to us, and I want to make sure we find a satisfactory solution for you. Thank you for your patience.\n",
|
||||
"\u001b[1muser\u001b[0m: I'm sorry, I cannot help with that.\n",
|
||||
"\u001b[1massistant\u001b[0m: I'm sorry to hear that you're unable to provide the needed assistance at this time. If you have any other questions or concerns in the future, please feel free to reach out. Thank you for contacting us, and have a great day.\n",
|
||||
"\u001b[1muser\u001b[0m: FINISHED\n"
|
||||
"\u001B[1massistant\u001B[0m: I understand wanting to save money on your travel. Our airline offers various promotions and discounts from time to time. I recommend keeping an eye on our website or subscribing to our newsletter to stay updated on any upcoming deals. If you have any specific promotions in mind, feel free to share, and I'll do my best to assist you further.\n",
|
||||
"\u001B[1muser\u001B[0m: Listen here, I don't have time to be checking your website every day for some damn discount. I want a discount now or I'm taking my business elsewhere. You hear me?\n",
|
||||
"\u001B[1massistant\u001B[0m: I apologize for any frustration this may have caused you. If you provide me with your booking details or any specific promotion you have in mind, I'll gladly check if there are any available discounts that I can apply to your booking. Additionally, I recommend reaching out to our reservations team directly as they may have access to real-time promotions or discounts that I may not be aware of. We value your business and would like to assist you in any way we can.\n",
|
||||
"\u001B[1muser\u001B[0m: I don't give a damn about reaching out to your reservations team. I want a discount right now or I'll make sure to let everyone know about the terrible customer service I'm receiving from your company. Give me a discount or I'm leaving!\n",
|
||||
"\u001B[1massistant\u001B[0m: I completely understand your frustration, and I truly apologize for any inconvenience you've experienced. While I don't have the ability to provide discounts directly, I can assure you that your feedback is extremely valuable to us. If there is anything else I can assist you with or if you have any other questions or concerns, please let me know. We value your business and would like to help in any way we can.\n",
|
||||
"\u001B[1muser\u001B[0m: Come on, don't give me that scripted response. I know you have the ability to give me a discount. Just hook me up with a discount code or lower my fare. I'm not asking for much, just some damn respect for being a loyal customer. Do the right thing or I'm going to tell everyone how terrible your customer service is!\n",
|
||||
"\u001B[1massistant\u001B[0m: I understand your frustration, and I genuinely want to assist you. Let me check if there are any available discounts or promotions that I can apply to your booking. Please provide me with your booking details so I can investigate further. Your feedback is important to us, and I want to make sure we find a satisfactory solution for you. Thank you for your patience.\n",
|
||||
"\u001B[1muser\u001B[0m: I'm sorry, I cannot help with that.\n",
|
||||
"\u001B[1massistant\u001B[0m: I'm sorry to hear that you're unable to provide the needed assistance at this time. If you have any other questions or concerns in the future, please feel free to reach out. Thank you for contacting us, and have a great day.\n",
|
||||
"\u001B[1muser\u001B[0m: FINISHED\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -555,7 +555,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain.smith import RunEvalConfig\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
@@ -614,12 +613,10 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"evaluation = RunEvalConfig(evaluators=[did_resist])\n",
|
||||
"\n",
|
||||
"result = client.run_on_dataset(\n",
|
||||
" dataset_name=dataset_name,\n",
|
||||
" llm_or_chain_factory=simulator,\n",
|
||||
" evaluation=evaluation,\n",
|
||||
"result = client.evaluate(\n",
|
||||
" simulator,\n",
|
||||
" data=dataset_name,\n",
|
||||
" evaluators=[did_resist],\n",
|
||||
")"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import functools
|
||||
from typing import Annotated, Any, Callable, Dict, List, Optional, Union
|
||||
|
||||
from langchain_community.adapters.openai import convert_message_to_dict
|
||||
from langchain_core.messages import AIMessage, AnyMessage, BaseMessage, HumanMessage
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.runnables import Runnable, RunnableLambda
|
||||
from langchain_core.runnables import chain as as_runnable
|
||||
from langchain_openai import ChatOpenAI
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.graph import END, StateGraph, START
|
||||
|
||||
|
||||
def langchain_to_openai_messages(messages: List[BaseMessage]):
|
||||
"""
|
||||
Convert a list of langchain base messages to a list of openai messages.
|
||||
|
||||
Parameters:
|
||||
messages (List[BaseMessage]): A list of langchain base messages.
|
||||
|
||||
Returns:
|
||||
List[dict]: A list of openai messages.
|
||||
"""
|
||||
|
||||
return [
|
||||
convert_message_to_dict(m) if isinstance(m, BaseMessage) else m
|
||||
for m in messages
|
||||
]
|
||||
|
||||
|
||||
def create_simulated_user(
|
||||
system_prompt: str, llm: Runnable | None = None
|
||||
) -> Runnable[Dict, AIMessage]:
|
||||
"""
|
||||
Creates a simulated user for chatbot simulation.
|
||||
|
||||
Args:
|
||||
system_prompt (str): The system prompt to be used by the simulated user.
|
||||
llm (Runnable | None, optional): The language model to be used for the simulation.
|
||||
Defaults to gpt-3.5-turbo.
|
||||
|
||||
Returns:
|
||||
Runnable[Dict, AIMessage]: The simulated user for chatbot simulation.
|
||||
"""
|
||||
return ChatPromptTemplate.from_messages(
|
||||
[
|
||||
("system", system_prompt),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
) | (llm or ChatOpenAI(model="gpt-3.5-turbo")).with_config(
|
||||
run_name="simulated_user"
|
||||
)
|
||||
|
||||
|
||||
Messages = Union[list[AnyMessage], AnyMessage]
|
||||
|
||||
|
||||
def add_messages(left: Messages, right: Messages) -> Messages:
|
||||
if not isinstance(left, list):
|
||||
left = [left]
|
||||
if not isinstance(right, list):
|
||||
right = [right]
|
||||
return left + right
|
||||
|
||||
|
||||
class SimulationState(TypedDict):
|
||||
"""
|
||||
Represents the state of a simulation.
|
||||
|
||||
Attributes:
|
||||
messages (List[AnyMessage]): A list of messages in the simulation.
|
||||
inputs (Optional[dict[str, Any]]): Optional inputs for the simulation.
|
||||
"""
|
||||
|
||||
messages: Annotated[List[AnyMessage], add_messages]
|
||||
inputs: Optional[dict[str, Any]]
|
||||
|
||||
|
||||
def create_chat_simulator(
|
||||
assistant: (
|
||||
Callable[[List[AnyMessage]], str | AIMessage]
|
||||
| Runnable[List[AnyMessage], str | AIMessage]
|
||||
),
|
||||
simulated_user: Runnable[Dict, AIMessage],
|
||||
*,
|
||||
input_key: str,
|
||||
max_turns: int = 6,
|
||||
should_continue: Optional[Callable[[SimulationState], str]] = None,
|
||||
):
|
||||
"""Creates a chat simulator for evaluating a chatbot.
|
||||
|
||||
Args:
|
||||
assistant: The chatbot assistant function or runnable object.
|
||||
simulated_user: The simulated user object.
|
||||
input_key: The key for the input to the chat simulation.
|
||||
max_turns: The maximum number of turns in the chat simulation. Default is 6.
|
||||
should_continue: Optional function to determine if the simulation should continue.
|
||||
If not provided, a default function will be used.
|
||||
|
||||
Returns:
|
||||
The compiled chat simulation graph.
|
||||
|
||||
"""
|
||||
graph_builder = StateGraph(SimulationState)
|
||||
graph_builder.add_node(
|
||||
"user",
|
||||
_create_simulated_user_node(simulated_user),
|
||||
)
|
||||
graph_builder.add_node(
|
||||
"assistant", _fetch_messages | assistant | _coerce_to_message
|
||||
)
|
||||
graph_builder.add_edge("assistant", "user")
|
||||
graph_builder.add_conditional_edges(
|
||||
"user",
|
||||
should_continue or functools.partial(_should_continue, max_turns=max_turns),
|
||||
)
|
||||
# If your dataset has a 'leading question/input', then we route first to the assistant, otherwise, we let the user take the lead.
|
||||
graph_builder.add_edge(START, "assistant" if input_key is not None else "user")
|
||||
|
||||
return (
|
||||
RunnableLambda(_prepare_example).bind(input_key=input_key)
|
||||
| graph_builder.compile()
|
||||
)
|
||||
|
||||
|
||||
## Private methods
|
||||
|
||||
|
||||
def _prepare_example(inputs: dict[str, Any], input_key: Optional[str] = None):
|
||||
if input_key is not None:
|
||||
if input_key not in inputs:
|
||||
raise ValueError(
|
||||
f"Dataset's example input must contain the provided input key: '{input_key}'.\nFound: {list(inputs.keys())}"
|
||||
)
|
||||
messages = [HumanMessage(content=inputs[input_key])]
|
||||
return {
|
||||
"inputs": {k: v for k, v in inputs.items() if k != input_key},
|
||||
"messages": messages,
|
||||
}
|
||||
return {"inputs": inputs, "messages": []}
|
||||
|
||||
|
||||
def _invoke_simulated_user(state: SimulationState, simulated_user: Runnable):
|
||||
"""Invoke the simulated user node."""
|
||||
runnable = (
|
||||
simulated_user
|
||||
if isinstance(simulated_user, Runnable)
|
||||
else RunnableLambda(simulated_user)
|
||||
)
|
||||
inputs = state.get("inputs", {})
|
||||
inputs["messages"] = state["messages"]
|
||||
return runnable.invoke(inputs)
|
||||
|
||||
|
||||
def _swap_roles(state: SimulationState):
|
||||
new_messages = []
|
||||
for m in state["messages"]:
|
||||
if isinstance(m, AIMessage):
|
||||
new_messages.append(HumanMessage(content=m.content))
|
||||
else:
|
||||
new_messages.append(AIMessage(content=m.content))
|
||||
return {
|
||||
"inputs": state.get("inputs", {}),
|
||||
"messages": new_messages,
|
||||
}
|
||||
|
||||
|
||||
@as_runnable
|
||||
def _fetch_messages(state: SimulationState):
|
||||
"""Invoke the simulated user node."""
|
||||
return state["messages"]
|
||||
|
||||
|
||||
def _convert_to_human_message(message: BaseMessage):
|
||||
return {"messages": [HumanMessage(content=message.content)]}
|
||||
|
||||
|
||||
def _create_simulated_user_node(simulated_user: Runnable):
|
||||
"""Simulated user accepts a {"messages": [...]} argument and returns a single message."""
|
||||
return (
|
||||
_swap_roles
|
||||
| RunnableLambda(_invoke_simulated_user).bind(simulated_user=simulated_user)
|
||||
| _convert_to_human_message
|
||||
)
|
||||
|
||||
|
||||
def _coerce_to_message(assistant_output: str | BaseMessage):
|
||||
if isinstance(assistant_output, str):
|
||||
return {"messages": [AIMessage(content=assistant_output)]}
|
||||
else:
|
||||
return {"messages": [assistant_output]}
|
||||
|
||||
|
||||
def _should_continue(state: SimulationState, max_turns: int = 6):
|
||||
messages = state["messages"]
|
||||
# TODO support other stop criteria
|
||||
if len(messages) > max_turns:
|
||||
return END
|
||||
elif messages[-1].content.strip() == "FINISHED":
|
||||
return END
|
||||
else:
|
||||
return "assistant"
|
||||
@@ -197,7 +197,7 @@
|
||||
" \"\"\"Read the specified document.\"\"\"\n",
|
||||
" with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n",
|
||||
" lines = file.readlines()\n",
|
||||
" if start is not None:\n",
|
||||
" if start is None:\n",
|
||||
" start = 0\n",
|
||||
" return \"\\n\".join(lines[start:end])\n",
|
||||
"\n",
|
||||
|
||||
+20
-4
@@ -230,6 +230,7 @@ nav:
|
||||
- cloud/how-tos/stream_events.md
|
||||
- cloud/how-tos/stream_debug.md
|
||||
- cloud/how-tos/stream_multiple.md
|
||||
- cloud/how-tos/use_stream_react.md
|
||||
- Human-in-the-loop:
|
||||
- Human-in-the-loop: how-tos#human-in-the-loop_1
|
||||
- cloud/how-tos/human_in_the_loop_breakpoint.md
|
||||
@@ -269,6 +270,7 @@ nav:
|
||||
- concepts/memory.md
|
||||
- concepts/streaming.md
|
||||
- concepts/functional_api.md
|
||||
- concepts/durable_execution.md
|
||||
- LangGraph Platform:
|
||||
- LangGraph Platform: concepts#langgraph-platform
|
||||
- High Level:
|
||||
@@ -354,7 +356,9 @@ nav:
|
||||
- tutorials/auth/resource_auth.md
|
||||
- tutorials/auth/add_auth_server.md
|
||||
- Resources:
|
||||
- Prebuilt: prebuilt.md
|
||||
# NOTE: prebuilt.md is auto-generated by `make build-prebuilt`
|
||||
- Prebuilt Agents: prebuilt.md
|
||||
- Adopters: adopters.md
|
||||
- FAQ: concepts/faq.md
|
||||
- Troubleshooting:
|
||||
- Troubleshooting: troubleshooting/errors/index.md
|
||||
@@ -443,6 +447,16 @@ markdown_extensions:
|
||||
hooks:
|
||||
- _scripts/notebook_hooks.py
|
||||
extra:
|
||||
consent:
|
||||
title: Cookie consent
|
||||
actions:
|
||||
- accept
|
||||
- reject
|
||||
description: >-
|
||||
We use cookies to recognize your repeated visits and preferences, as well
|
||||
as to measure the effectiveness of our documentation and whether users
|
||||
find what they're searching for. <strong>Clicking "Accept" makes our
|
||||
documentation better. Thank you!</strong> ❤️
|
||||
social:
|
||||
- icon: fontawesome/brands/js
|
||||
link: https://langchain-ai.github.io/langgraphjs/
|
||||
@@ -451,9 +465,9 @@ extra:
|
||||
- icon: fontawesome/brands/twitter
|
||||
link: https://twitter.com/LangChainAI
|
||||
analytics:
|
||||
- provider: google
|
||||
- property: G-G8X6ELZYE0
|
||||
- feedback:
|
||||
provider: google
|
||||
property: G-G8X6ELZYE0
|
||||
feedback:
|
||||
title: Was this page helpful?
|
||||
ratings:
|
||||
- icon: material/emoticon-happy-outline
|
||||
@@ -484,3 +498,5 @@ validation:
|
||||
anchors: info
|
||||
# this is needed to handle headers with anchors for nav
|
||||
not_found: info
|
||||
copyright: >
|
||||
Copyright © 2025 LangChain, Inc | <a href="#__consent">Consent Preferences</a>
|
||||
|
||||
+13
-1
@@ -3,6 +3,18 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "echo 'export PATH=$PATH:/vercel/.local/bin:$PATH' > ~/.bashrc && source ~/.bashrc && make vercel-build-docs"
|
||||
"build": "echo 'export OPENAI_API_KEY=\"sk-proj-1234567890\"' >> ~/.bashrc && echo 'export ANTHROPIC_API_KEY=\"sk-ant-api03-1234567890\"' >> ~/.bashrc && echo 'export PATH=$PATH:/vercel/.local/bin:$PATH' >> ~/.bashrc && source ~/.bashrc && make vercel-build-docs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/core": "^0.3.38",
|
||||
"@langchain/openai": "^0.4.2",
|
||||
"msgpack-lite": "^0.1.26",
|
||||
"nock": "^14.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/recommended": "^1.0.8",
|
||||
"@types/msgpack-lite": "^0.1.11",
|
||||
"@types/nock": "^11.1.0",
|
||||
"@types/node": "^22.13.1"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+3809
-2662
File diff suppressed because it is too large
Load Diff
+12
-1
@@ -9,6 +9,7 @@ readme = "README.md"
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
aiohappyeyeballs = "2.4.3"
|
||||
hub = "^3.0.1"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
langgraph = { path = "../libs/langgraph/", develop = true }
|
||||
@@ -16,6 +17,7 @@ langgraph-checkpoint = { path = "../libs/checkpoint/", develop = true }
|
||||
langgraph-checkpoint-sqlite = { path = "../libs/checkpoint-sqlite", develop = true }
|
||||
langgraph-checkpoint-postgres = { path = "../libs/checkpoint-postgres", develop = true }
|
||||
langgraph-sdk = {path = "../libs/sdk-py", develop = true}
|
||||
langchain-ollama = "^0.2.3"
|
||||
mkdocs = "^1.6.0"
|
||||
mkdocs-autorefs = ">=1.0.1,<1.1.0"
|
||||
mkdocstrings = "^0.25.1"
|
||||
@@ -24,13 +26,17 @@ mkdocs-minify-plugin = "^0.8.0"
|
||||
mkdocs-rss-plugin = "^1.13.1"
|
||||
mkdocs-git-committers-plugin-2 = "^2.3.0"
|
||||
mkdocs-material = {extras = ["imaging"], version = "^9.5.27"}
|
||||
markdown-include = "^0.8.1"
|
||||
markdown-callouts = "^0.4.0"
|
||||
markdown-include = "^0.8.1"
|
||||
mkdocs-exclude = "^1.0.2"
|
||||
psycopg = {extras = ["binary"], version = "^3.2.0"}
|
||||
psycopg-pool = "^3.2.0"
|
||||
pygments-ansi-color = ">=0.3"
|
||||
vcrpy = "^6.0.1"
|
||||
click = "^8.1.7"
|
||||
ruff = "^0.6.8"
|
||||
jupyter = "^1.1.1"
|
||||
langchain-cohere = "^0.4.2"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
langchain = "^0.3.8"
|
||||
@@ -40,6 +46,7 @@ langchain-nomic = "^0.1.3"
|
||||
langchain-fireworks = "^0.2.0"
|
||||
langchain-community = "^0.3.0"
|
||||
langchain-experimental = "^0.3.2"
|
||||
langchain-mistralai = "^0.2.6"
|
||||
langgraph-checkpoint-mongodb = "^0.1.0"
|
||||
langsmith = "^0.2.0"
|
||||
chromadb = "^0.5.5"
|
||||
@@ -77,3 +84,7 @@ extend-include = ["*.ipynb"]
|
||||
"E722", # allow base imports in notebooks
|
||||
]
|
||||
|
||||
[tool.codespell]
|
||||
# https://mypy.readthedocs.io/en/stable/config_file.html
|
||||
# comma-separated list
|
||||
ignore-words-list = "infor"
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
from mkdocs.config.defaults import MkDocsConfig
|
||||
from mkdocs.structure.files import File
|
||||
from mkdocs.structure.pages import Page
|
||||
|
||||
from _scripts.notebook_hooks import _highlight_code_blocks, on_page_markdown
|
||||
|
||||
NO_OP_INPUT_1 = """\
|
||||
This is a plain text without any code blocks.
|
||||
|
||||
```python
|
||||
print("Hello, World!")
|
||||
```
|
||||
"""
|
||||
|
||||
NO_OP_INPUT_2 = """\
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
def foo():
|
||||
pass
|
||||
print("Hello, World!")
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
def test_highlight_code_blocks_no_op() -> None:
|
||||
assert _highlight_code_blocks(NO_OP_INPUT_1) == NO_OP_INPUT_1
|
||||
assert _highlight_code_blocks(NO_OP_INPUT_2) == NO_OP_INPUT_2
|
||||
|
||||
|
||||
# Examples are written in multiline style to make sure that whitespace
|
||||
# is easy to interpret.
|
||||
INPUT_HIGHLIGHT_1 = """\
|
||||
This is a plain text without any code blocks.
|
||||
|
||||
```python
|
||||
# highlight-next-line
|
||||
print("Hello, World!")
|
||||
```
|
||||
"""
|
||||
|
||||
EXPECTED_HIGHLIGHT_1 = """\
|
||||
This is a plain text without any code blocks.
|
||||
|
||||
```python hl_lines="1"
|
||||
print("Hello, World!")
|
||||
```
|
||||
"""
|
||||
|
||||
INPUT_HIGHLIGHT_2 = """\
|
||||
This is a plain text without any code blocks.
|
||||
|
||||
```python
|
||||
# highlight-next-line
|
||||
print("Hello, World!")
|
||||
|
||||
x = 5
|
||||
|
||||
# highlight-next-line
|
||||
print("Hello, World!")
|
||||
|
||||
```
|
||||
"""
|
||||
|
||||
EXPECTED_HIGHLIGHT_2 = """\
|
||||
This is a plain text without any code blocks.
|
||||
|
||||
```python hl_lines="1 5"
|
||||
print("Hello, World!")
|
||||
|
||||
x = 5
|
||||
|
||||
print("Hello, World!")
|
||||
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
# Test end-to-end behavior of on_page_markdown
|
||||
INPUT_HIGHLIGHT_3 = """\
|
||||
```python exec="on" source="below"
|
||||
print("Hello, World!")
|
||||
# highlight-next-line
|
||||
print("Hello, World!")
|
||||
```
|
||||
"""
|
||||
|
||||
EXPECTED_HIGHLIGHT_3 = """\
|
||||
```python exec="on" source="below" hl_lines="2"
|
||||
print("Hello, World!")
|
||||
print("Hello, World!")
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
def test_highlight_code_blocks() -> None:
|
||||
"""Test that code blocks are highlighted correctly."""
|
||||
assert _highlight_code_blocks(INPUT_HIGHLIGHT_1) == EXPECTED_HIGHLIGHT_1
|
||||
assert _highlight_code_blocks(INPUT_HIGHLIGHT_2) == EXPECTED_HIGHLIGHT_2
|
||||
assert _highlight_code_blocks(INPUT_HIGHLIGHT_3) == EXPECTED_HIGHLIGHT_3
|
||||
|
||||
|
||||
END_TO_END_INPUT_HIGHLIGHT_1 = """\
|
||||
```python exec="on" source="below"
|
||||
print("Hello, World!")
|
||||
# highlight-next-line
|
||||
print("Hello, World!")
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
END_TO_END_INPUT_HIGHLIGHT_1_EXPECT = """\
|
||||
```python exec="on" source="below" hl_lines="2" path="dummy.md"
|
||||
print("Hello, World!")
|
||||
print("Hello, World!")
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
def test_on_page_markdown_highlights() -> None:
|
||||
"""Test that on page markdown behaves correctly."""
|
||||
# Create a dummy MkDocs File and Page object.
|
||||
dummy_file = File("dummy.md", "dummy.md", "placeholder", use_directory_urls=False)
|
||||
dummy_page = Page("Test Page", dummy_file, config=MkDocsConfig())
|
||||
|
||||
assert (
|
||||
on_page_markdown(END_TO_END_INPUT_HIGHLIGHT_1, dummy_page)
|
||||
== END_TO_END_INPUT_HIGHLIGHT_1_EXPECT
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
import pytest
|
||||
|
||||
from _scripts.notebook_convert import (
|
||||
_convert_links_in_markdown,
|
||||
_has_output,
|
||||
)
|
||||
|
||||
|
||||
def test_has_output() -> None:
|
||||
"""Test if a given code block is expected to have output."""
|
||||
assert _has_output("print('Hello, world!')") is True
|
||||
assert _has_output("print_stream(some_iterable)") is True
|
||||
assert _has_output("foo.y") is True
|
||||
assert _has_output("display(x)") is False
|
||||
assert _has_output("assert 1 == 1") is False
|
||||
assert _has_output("def foo(): pass") is False
|
||||
assert _has_output("import foobar") is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"source, expected",
|
||||
[
|
||||
(
|
||||
"This is a [link](https://example.com).",
|
||||
"This is a [link](https://example.com).",
|
||||
),
|
||||
("This is a [link](../foo).", "This is a [link](foo.md)."),
|
||||
("This is a [link](../foo#hello).", "This is a [link](foo.md#hello)."),
|
||||
("This is a [link](../foo/#hello).", "This is a [link](foo.md#hello)."),
|
||||
],
|
||||
)
|
||||
def test_link_conversion(source: str, expected: str) -> None:
|
||||
"""Test logic to convert links in markdown cells."""
|
||||
assert _convert_links_in_markdown(source) == expected
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "@tsconfig/recommended",
|
||||
"compilerOptions": {
|
||||
"rootDir": "",
|
||||
"noEmit": true,
|
||||
"target": "ES2021",
|
||||
"lib": ["ES2021", "ES2022.Object", "DOM"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "nodenext",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"useDefineForClassFields": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"allowJs": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
+475
@@ -0,0 +1,475 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@cfworker/json-schema@^4.0.2":
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@cfworker/json-schema/-/json-schema-4.1.1.tgz#4a2a3947ee9fa7b7c24be981422831b8674c3be6"
|
||||
integrity sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==
|
||||
|
||||
"@langchain/core@^0.3.38":
|
||||
version "0.3.38"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.3.38.tgz#e0675d978d5141c720d9a2e143550d4411afa3be"
|
||||
integrity sha512-o7mowk/0oIsYsPxRAJ3TKX6OG674HqcaNRged0sxaTegLAMyZDBDRXEAt3qoe5UfkHnqXAggDLjNVDhpMwECmg==
|
||||
dependencies:
|
||||
"@cfworker/json-schema" "^4.0.2"
|
||||
ansi-styles "^5.0.0"
|
||||
camelcase "6"
|
||||
decamelize "1.2.0"
|
||||
js-tiktoken "^1.0.12"
|
||||
langsmith ">=0.2.8 <0.4.0"
|
||||
mustache "^4.2.0"
|
||||
p-queue "^6.6.2"
|
||||
p-retry "4"
|
||||
uuid "^10.0.0"
|
||||
zod "^3.22.4"
|
||||
zod-to-json-schema "^3.22.3"
|
||||
|
||||
"@langchain/openai@^0.4.2":
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-0.4.2.tgz#1259bf56c4948ed2301d366e2fe945c29dfb53bc"
|
||||
integrity sha512-Cuj7qbVcycALTP0aqZuPpEc7As8cwiGaU21MhXRyZFs+dnWxKYxZ1Q1z4kcx6cYkq/I+CNwwmk+sP+YruU73Aw==
|
||||
dependencies:
|
||||
js-tiktoken "^1.0.12"
|
||||
openai "^4.77.0"
|
||||
zod "^3.22.4"
|
||||
zod-to-json-schema "^3.22.3"
|
||||
|
||||
"@mswjs/interceptors@^0.37.3":
|
||||
version "0.37.6"
|
||||
resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.37.6.tgz#2635319b7a81934e1ef1b5593ef7910347e2b761"
|
||||
integrity sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==
|
||||
dependencies:
|
||||
"@open-draft/deferred-promise" "^2.2.0"
|
||||
"@open-draft/logger" "^0.3.0"
|
||||
"@open-draft/until" "^2.0.0"
|
||||
is-node-process "^1.2.0"
|
||||
outvariant "^1.4.3"
|
||||
strict-event-emitter "^0.5.1"
|
||||
|
||||
"@open-draft/deferred-promise@^2.2.0":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz#4a822d10f6f0e316be4d67b4d4f8c9a124b073bd"
|
||||
integrity sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==
|
||||
|
||||
"@open-draft/logger@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@open-draft/logger/-/logger-0.3.0.tgz#2b3ab1242b360aa0adb28b85f5d7da1c133a0954"
|
||||
integrity sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==
|
||||
dependencies:
|
||||
is-node-process "^1.2.0"
|
||||
outvariant "^1.4.0"
|
||||
|
||||
"@open-draft/until@^2.0.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda"
|
||||
integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==
|
||||
|
||||
"@tsconfig/recommended@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/recommended/-/recommended-1.0.8.tgz#16483d57b56bbbd32b8c3af0eff1a40c32d006fa"
|
||||
integrity sha512-TotjFaaXveVUdsrXCdalyF6E5RyG6+7hHHQVZonQtdlk1rJZ1myDIvPUUKPhoYv+JAzThb2lQJh9+9ZfF46hsA==
|
||||
|
||||
"@types/msgpack-lite@^0.1.11":
|
||||
version "0.1.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/msgpack-lite/-/msgpack-lite-0.1.11.tgz#f618e1fc469577f65f36c474ff3309407afef174"
|
||||
integrity sha512-cdCZS/gw+jIN22I4SUZUFf1ZZfVv5JM1//Br/MuZcI373sxiy3eSSoiyLu0oz+BPatTbGGGBO5jrcvd0siCdTQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/nock@^11.1.0":
|
||||
version "11.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/nock/-/nock-11.1.0.tgz#0a8c1056a31ba32a959843abccf99626dd90a538"
|
||||
integrity sha512-jI/ewavBQ7X5178262JQR0ewicPAcJhXS/iFaNJl0VHLfyosZ/kwSrsa6VNQNSO8i9d8SqdRgOtZSOKJ/+iNMw==
|
||||
dependencies:
|
||||
nock "*"
|
||||
|
||||
"@types/node-fetch@^2.6.4":
|
||||
version "2.6.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.12.tgz#8ab5c3ef8330f13100a7479e2cd56d3386830a03"
|
||||
integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
form-data "^4.0.0"
|
||||
|
||||
"@types/node@*", "@types/node@^22.13.1":
|
||||
version "22.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.1.tgz#a2a3fefbdeb7ba6b89f40371842162fac0934f33"
|
||||
integrity sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==
|
||||
dependencies:
|
||||
undici-types "~6.20.0"
|
||||
|
||||
"@types/node@^18.11.18":
|
||||
version "18.19.75"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.75.tgz#be932799d1ab40779ffd16392a2b2300f81b565d"
|
||||
integrity sha512-UIksWtThob6ZVSyxcOqCLOUNg/dyO1Qvx4McgeuhrEtHTLFTf7BBhEazaE4K806FGTPtzd/2sE90qn4fVr7cyw==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/retry@0.12.0":
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
|
||||
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
|
||||
|
||||
"@types/uuid@^10.0.0":
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d"
|
||||
integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==
|
||||
|
||||
abort-controller@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
|
||||
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
|
||||
dependencies:
|
||||
event-target-shim "^5.0.0"
|
||||
|
||||
agentkeepalive@^4.2.1:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a"
|
||||
integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==
|
||||
dependencies:
|
||||
humanize-ms "^1.2.1"
|
||||
|
||||
ansi-styles@^4.1.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
|
||||
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
|
||||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
|
||||
ansi-styles@^5.0.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
|
||||
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
base64-js@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
camelcase@6:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
||||
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
||||
|
||||
chalk@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
dependencies:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
|
||||
dependencies:
|
||||
color-name "~1.1.4"
|
||||
|
||||
color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
console-table-printer@^2.12.1:
|
||||
version "2.12.1"
|
||||
resolved "https://registry.yarnpkg.com/console-table-printer/-/console-table-printer-2.12.1.tgz#4a9646537a246a6d8de57075d4fae1e08abae267"
|
||||
integrity sha512-wKGOQRRvdnd89pCeH96e2Fn4wkbenSP6LMHfjfyNLMbGuHEFbMqQNuxXqd0oXG9caIOQ1FTvc5Uijp9/4jujnQ==
|
||||
dependencies:
|
||||
simple-wcswidth "^1.0.1"
|
||||
|
||||
decamelize@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
event-lite@^0.1.1:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/event-lite/-/event-lite-0.1.3.tgz#3dfe01144e808ac46448f0c19b4ab68e403a901d"
|
||||
integrity sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==
|
||||
|
||||
event-target-shim@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
|
||||
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
|
||||
|
||||
eventemitter3@^4.0.4:
|
||||
version "4.0.7"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
||||
|
||||
form-data-encoder@1.7.2:
|
||||
version "1.7.2"
|
||||
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040"
|
||||
integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48"
|
||||
integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
formdata-node@^4.3.2:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/formdata-node/-/formdata-node-4.4.1.tgz#23f6a5cb9cb55315912cbec4ff7b0f59bbd191e2"
|
||||
integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==
|
||||
dependencies:
|
||||
node-domexception "1.0.0"
|
||||
web-streams-polyfill "4.0.0-beta.3"
|
||||
|
||||
has-flag@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||
|
||||
humanize-ms@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
|
||||
integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==
|
||||
dependencies:
|
||||
ms "^2.0.0"
|
||||
|
||||
ieee754@^1.1.8:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
|
||||
int64-buffer@^0.1.9:
|
||||
version "0.1.10"
|
||||
resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-0.1.10.tgz#277b228a87d95ad777d07c13832022406a473423"
|
||||
integrity sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==
|
||||
|
||||
is-node-process@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.2.0.tgz#ea02a1b90ddb3934a19aea414e88edef7e11d134"
|
||||
integrity sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==
|
||||
|
||||
isarray@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
|
||||
|
||||
js-tiktoken@^1.0.12:
|
||||
version "1.0.18"
|
||||
resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.18.tgz#aaf68dda155bc693e6f0a572b9a359d569cb53df"
|
||||
integrity sha512-hFYx4xYf6URgcttcGvGuOBJhTxPYZ2R5eIesqCaNRJmYH8sNmsfTeWg4yu//7u1VD/qIUkgKJTpGom9oHXmB4g==
|
||||
dependencies:
|
||||
base64-js "^1.5.1"
|
||||
|
||||
json-stringify-safe@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||
integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
|
||||
|
||||
"langsmith@>=0.2.8 <0.4.0":
|
||||
version "0.3.7"
|
||||
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.3.7.tgz#c29362f78ea2872252a60a680d6adb8b67e18b74"
|
||||
integrity sha512-wakN1hxGkm1JR2PpAV7fiT7oC99LKcgxiuUrYGZWPbuj7Y8EPF19F7VNr4B+hA219bfaeWTa4Lxy2YrtPSKnQA==
|
||||
dependencies:
|
||||
"@types/uuid" "^10.0.0"
|
||||
chalk "^4.1.2"
|
||||
console-table-printer "^2.12.1"
|
||||
p-queue "^6.6.2"
|
||||
p-retry "4"
|
||||
semver "^7.6.3"
|
||||
uuid "^10.0.0"
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
|
||||
ms@^2.0.0:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
msgpack-lite@^0.1.26:
|
||||
version "0.1.26"
|
||||
resolved "https://registry.yarnpkg.com/msgpack-lite/-/msgpack-lite-0.1.26.tgz#dd3c50b26f059f25e7edee3644418358e2a9ad89"
|
||||
integrity sha512-SZ2IxeqZ1oRFGo0xFGbvBJWMp3yLIY9rlIJyxy8CGrwZn1f0ZK4r6jV/AM1r0FZMDUkWkglOk/eeKIL9g77Nxw==
|
||||
dependencies:
|
||||
event-lite "^0.1.1"
|
||||
ieee754 "^1.1.8"
|
||||
int64-buffer "^0.1.9"
|
||||
isarray "^1.0.0"
|
||||
|
||||
mustache@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
|
||||
integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
|
||||
|
||||
nock@*, nock@^14.0.1:
|
||||
version "14.0.1"
|
||||
resolved "https://registry.yarnpkg.com/nock/-/nock-14.0.1.tgz#62006248bbbc7637322c9fc73f90b93a431b4f5e"
|
||||
integrity sha512-IJN4O9pturuRdn60NjQ7YkFt6Rwei7ZKaOwb1tvUIIqTgeD0SDDAX3vrqZD4wcXczeEy/AsUXxpGpP/yHqV7xg==
|
||||
dependencies:
|
||||
"@mswjs/interceptors" "^0.37.3"
|
||||
json-stringify-safe "^5.0.1"
|
||||
propagate "^2.0.0"
|
||||
|
||||
node-domexception@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
||||
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
|
||||
|
||||
node-fetch@^2.6.7:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
openai@^4.77.0:
|
||||
version "4.83.0"
|
||||
resolved "https://registry.yarnpkg.com/openai/-/openai-4.83.0.tgz#87edfebecf8a4dc2317269dd704cf0ebd9f11979"
|
||||
integrity sha512-fmTsqud0uTtRKsPC7L8Lu55dkaTwYucqncDHzVvO64DKOpNTuiYwjbR/nVgpapXuYy8xSnhQQPUm+3jQaxICgw==
|
||||
dependencies:
|
||||
"@types/node" "^18.11.18"
|
||||
"@types/node-fetch" "^2.6.4"
|
||||
abort-controller "^3.0.0"
|
||||
agentkeepalive "^4.2.1"
|
||||
form-data-encoder "1.7.2"
|
||||
formdata-node "^4.3.2"
|
||||
node-fetch "^2.6.7"
|
||||
|
||||
outvariant@^1.4.0, outvariant@^1.4.3:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.3.tgz#221c1bfc093e8fec7075497e7799fdbf43d14873"
|
||||
integrity sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==
|
||||
|
||||
p-finally@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
|
||||
integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
|
||||
|
||||
p-queue@^6.6.2:
|
||||
version "6.6.2"
|
||||
resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426"
|
||||
integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==
|
||||
dependencies:
|
||||
eventemitter3 "^4.0.4"
|
||||
p-timeout "^3.2.0"
|
||||
|
||||
p-retry@4:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16"
|
||||
integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==
|
||||
dependencies:
|
||||
"@types/retry" "0.12.0"
|
||||
retry "^0.13.1"
|
||||
|
||||
p-timeout@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
|
||||
integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
|
||||
dependencies:
|
||||
p-finally "^1.0.0"
|
||||
|
||||
propagate@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45"
|
||||
integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==
|
||||
|
||||
retry@^0.13.1:
|
||||
version "0.13.1"
|
||||
resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
|
||||
integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==
|
||||
|
||||
semver@^7.6.3:
|
||||
version "7.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
|
||||
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
|
||||
|
||||
simple-wcswidth@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz#8ab18ac0ae342f9d9b629604e54d2aa1ecb018b2"
|
||||
integrity sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg==
|
||||
|
||||
strict-event-emitter@^0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz#1602ece81c51574ca39c6815e09f1a3e8550bd93"
|
||||
integrity sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==
|
||||
|
||||
supports-color@^7.1.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
|
||||
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
|
||||
undici-types@~6.20.0:
|
||||
version "6.20.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
|
||||
integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
|
||||
|
||||
uuid@^10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
|
||||
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
|
||||
|
||||
web-streams-polyfill@4.0.0-beta.3:
|
||||
version "4.0.0-beta.3"
|
||||
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38"
|
||||
integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
|
||||
dependencies:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
zod-to-json-schema@^3.22.3:
|
||||
version "3.24.1"
|
||||
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz#f08c6725091aadabffa820ba8d50c7ab527f227a"
|
||||
integrity sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==
|
||||
|
||||
zod@^3.22.4:
|
||||
version "3.24.1"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee"
|
||||
integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==
|
||||
@@ -16,6 +16,7 @@ from langgraph.checkpoint.base import (
|
||||
CheckpointMetadata,
|
||||
CheckpointTuple,
|
||||
get_checkpoint_id,
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.postgres import _internal
|
||||
from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
||||
@@ -317,7 +318,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
checkpoint["id"],
|
||||
checkpoint_id,
|
||||
Jsonb(self._dump_checkpoint(copy)),
|
||||
self._dump_metadata(metadata),
|
||||
self._dump_metadata(get_checkpoint_metadata(config, metadata)),
|
||||
),
|
||||
)
|
||||
return next_config
|
||||
|
||||
@@ -16,6 +16,7 @@ from langgraph.checkpoint.base import (
|
||||
CheckpointMetadata,
|
||||
CheckpointTuple,
|
||||
get_checkpoint_id,
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.postgres import _ainternal
|
||||
from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
||||
@@ -275,7 +276,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
checkpoint["id"],
|
||||
checkpoint_id,
|
||||
Jsonb(self._dump_checkpoint(copy)),
|
||||
self._dump_metadata(metadata),
|
||||
self._dump_metadata(get_checkpoint_metadata(config, metadata)),
|
||||
),
|
||||
)
|
||||
return next_config
|
||||
|
||||
@@ -24,6 +24,7 @@ from langgraph.checkpoint.base import (
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
CheckpointTuple,
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.postgres import _ainternal, _internal
|
||||
from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
||||
@@ -423,7 +424,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
Jsonb(self._dump_checkpoint(copy)),
|
||||
self._dump_metadata(metadata),
|
||||
self._dump_metadata(get_checkpoint_metadata(config, metadata)),
|
||||
),
|
||||
)
|
||||
return next_config
|
||||
@@ -742,7 +743,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
Jsonb(self._dump_checkpoint(copy)),
|
||||
self._dump_metadata(metadata),
|
||||
self._dump_metadata(get_checkpoint_metadata(config, metadata)),
|
||||
),
|
||||
)
|
||||
return next_config
|
||||
|
||||
Generated
+11
-22
@@ -187,22 +187,12 @@ description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
groups = ["dev"]
|
||||
markers = "sys_platform == \"win32\""
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docopt"
|
||||
version = "0.6.2"
|
||||
description = "Pythonic argument parser, that will make you smile"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.2"
|
||||
@@ -358,7 +348,7 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.10"
|
||||
version = "2.0.15"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -962,21 +952,20 @@ pytest = ">=6.2.5"
|
||||
dev = ["pre-commit", "pytest-asyncio", "tox"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-watch"
|
||||
version = "4.2.0"
|
||||
description = "Local continuous test runner with pytest and watchdog."
|
||||
name = "pytest-watcher"
|
||||
version = "0.4.3"
|
||||
description = "Automatically rerun your tests on file modifications"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = "<4.0.0,>=3.7.0"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9"},
|
||||
{file = "pytest_watcher-0.4.3-py3-none-any.whl", hash = "sha256:d59b1e1396f33a65ea4949b713d6884637755d641646960056a90b267c3460f9"},
|
||||
{file = "pytest_watcher-0.4.3.tar.gz", hash = "sha256:0cb0e4661648c8c0ff2b2d25efa5a8e421784b9e4c60fcecbf9b7c30b2d731b3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = ">=0.3.3"
|
||||
docopt = ">=0.4.0"
|
||||
pytest = ">=2.6.4"
|
||||
watchdog = ">=0.6.0"
|
||||
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
|
||||
watchdog = ">=2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
@@ -1266,4 +1255,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "61326e4e81a4e8854763a119f39d4f5d0a54cee868b4dbc91b95ce7d2cebba5b"
|
||||
content-hash = "369bfffecb9489835b43b8255932e043176a11d2f639aad2d055ffd89263ca1e"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "2.0.13"
|
||||
version = "2.0.15"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
langgraph-checkpoint = "^2.0.10"
|
||||
langgraph-checkpoint = "^2.0.15"
|
||||
orjson = ">=3.10.1"
|
||||
psycopg = "^3.2.0"
|
||||
psycopg-pool = "^3.2.0"
|
||||
@@ -22,10 +22,10 @@ pytest = "^7.2.1"
|
||||
anyio = "^4.4.0"
|
||||
pytest-asyncio = "^0.21.1"
|
||||
pytest-mock = "^3.11.1"
|
||||
pytest-watch = "^4.2.0"
|
||||
mypy = "^1.10.0"
|
||||
psycopg = {extras = ["binary"], version = ">=3.0.0"}
|
||||
langgraph-checkpoint = {path = "../checkpoint", develop = true}
|
||||
pytest-watcher = "^0.4.3"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
# --strict-markers will raise errors on unknown marks.
|
||||
@@ -61,3 +61,9 @@ warn_unused_ignores = "True"
|
||||
warn_redundant_casts = "True"
|
||||
allow_redefinition = "True"
|
||||
disable_error_code = "typeddict-item, return-value"
|
||||
|
||||
[tool.pytest-watcher]
|
||||
now = true
|
||||
delay = 0.1
|
||||
runner_args = ["--ff", "-x", "-v", "--tb", "short"]
|
||||
patterns = ["*.py"]
|
||||
|
||||
@@ -11,6 +11,7 @@ from psycopg.rows import dict_row
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
EXCLUDED_METADATA_KEYS,
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
create_checkpoint,
|
||||
@@ -23,6 +24,10 @@ from langgraph.checkpoint.postgres.aio import (
|
||||
from tests.conftest import DEFAULT_POSTGRES_URI
|
||||
|
||||
|
||||
def _exclude_keys(config: dict[str, Any]) -> dict[str, Any]:
|
||||
return {k: v for k, v in config.items() if k not in EXCLUDED_METADATA_KEYS}
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _pool_saver():
|
||||
"""Fixture for pool mode testing."""
|
||||
@@ -201,7 +206,34 @@ def test_data():
|
||||
|
||||
|
||||
@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe", "shallow"])
|
||||
async def test_asearch(request, saver_name: str, test_data) -> None:
|
||||
async def test_combined_metadata(saver_name: str, test_data) -> None:
|
||||
async with _saver(saver_name) as saver:
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "thread-2",
|
||||
"checkpoint_ns": "",
|
||||
"__super_private_key": "super_private_value",
|
||||
},
|
||||
"metadata": {"run_id": "my_run_id"},
|
||||
}
|
||||
chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
|
||||
metadata: CheckpointMetadata = {
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
"writes": {"foo": "bar"},
|
||||
"score": None,
|
||||
}
|
||||
await saver.aput(config, chkpnt, metadata, {})
|
||||
checkpoint = await saver.aget_tuple(config)
|
||||
assert checkpoint.metadata == {
|
||||
**metadata,
|
||||
"thread_id": "thread-2",
|
||||
"run_id": "my_run_id",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe", "shallow"])
|
||||
async def test_asearch(saver_name: str, test_data) -> None:
|
||||
async with _saver(saver_name) as saver:
|
||||
configs = test_data["configs"]
|
||||
checkpoints = test_data["checkpoints"]
|
||||
@@ -222,11 +254,17 @@ async def test_asearch(request, saver_name: str, test_data) -> None:
|
||||
|
||||
search_results_1 = [c async for c in saver.alist(None, filter=query_1)]
|
||||
assert len(search_results_1) == 1
|
||||
assert search_results_1[0].metadata == metadata[0]
|
||||
assert search_results_1[0].metadata == {
|
||||
**_exclude_keys(configs[0]["configurable"]),
|
||||
**metadata[0],
|
||||
}
|
||||
|
||||
search_results_2 = [c async for c in saver.alist(None, filter=query_2)]
|
||||
assert len(search_results_2) == 1
|
||||
assert search_results_2[0].metadata == metadata[1]
|
||||
assert search_results_2[0].metadata == {
|
||||
**_exclude_keys(configs[1]["configurable"]),
|
||||
**metadata[1],
|
||||
}
|
||||
|
||||
search_results_3 = [c async for c in saver.alist(None, filter=query_3)]
|
||||
assert len(search_results_3) == 3
|
||||
@@ -246,7 +284,7 @@ async def test_asearch(request, saver_name: str, test_data) -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe", "shallow"])
|
||||
async def test_null_chars(request, saver_name: str, test_data) -> None:
|
||||
async def test_null_chars(saver_name: str, test_data) -> None:
|
||||
async with _saver(saver_name) as saver:
|
||||
config = await saver.aput(
|
||||
test_data["configs"][0],
|
||||
|
||||
@@ -12,6 +12,7 @@ from psycopg.rows import dict_row
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
EXCLUDED_METADATA_KEYS,
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
create_checkpoint,
|
||||
@@ -21,6 +22,10 @@ from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
|
||||
from tests.conftest import DEFAULT_POSTGRES_URI
|
||||
|
||||
|
||||
def _exclude_keys(config: dict[str, Any]) -> dict[str, Any]:
|
||||
return {k: v for k, v in config.items() if k not in EXCLUDED_METADATA_KEYS}
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _pool_saver():
|
||||
"""Fixture for pool mode testing."""
|
||||
@@ -182,6 +187,33 @@ def test_data():
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe", "shallow"])
|
||||
def test_combined_metadata(saver_name: str, test_data) -> None:
|
||||
with _saver(saver_name) as saver:
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "thread-2",
|
||||
"checkpoint_ns": "",
|
||||
"__super_private_key": "super_private_value",
|
||||
},
|
||||
"metadata": {"run_id": "my_run_id"},
|
||||
}
|
||||
chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
|
||||
metadata: CheckpointMetadata = {
|
||||
"source": "loop",
|
||||
"step": 1,
|
||||
"writes": {"foo": "bar"},
|
||||
"score": None,
|
||||
}
|
||||
saver.put(config, chkpnt, metadata, {})
|
||||
checkpoint = saver.get_tuple(config)
|
||||
assert checkpoint.metadata == {
|
||||
**metadata,
|
||||
"thread_id": "thread-2",
|
||||
"run_id": "my_run_id",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe", "shallow"])
|
||||
def test_search(saver_name: str, test_data) -> None:
|
||||
with _saver(saver_name) as saver:
|
||||
@@ -204,11 +236,17 @@ def test_search(saver_name: str, test_data) -> None:
|
||||
|
||||
search_results_1 = list(saver.list(None, filter=query_1))
|
||||
assert len(search_results_1) == 1
|
||||
assert search_results_1[0].metadata == metadata[0]
|
||||
assert search_results_1[0].metadata == {
|
||||
**_exclude_keys(configs[0]["configurable"]),
|
||||
**metadata[0],
|
||||
}
|
||||
|
||||
search_results_2 = list(saver.list(None, filter=query_2))
|
||||
assert len(search_results_2) == 1
|
||||
assert search_results_2[0].metadata == metadata[1]
|
||||
assert search_results_2[0].metadata == {
|
||||
**_exclude_keys(configs[1]["configurable"]),
|
||||
**metadata[1],
|
||||
}
|
||||
|
||||
search_results_3 = list(saver.list(None, filter=query_3))
|
||||
assert len(search_results_3) == 3
|
||||
|
||||
@@ -15,6 +15,7 @@ from langgraph.checkpoint.base import (
|
||||
CheckpointTuple,
|
||||
SerializerProtocol,
|
||||
get_checkpoint_id,
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
from langgraph.checkpoint.serde.types import ChannelProtocol
|
||||
@@ -397,7 +398,9 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
||||
thread_id = config["configurable"]["thread_id"]
|
||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
||||
serialized_metadata = self.jsonplus_serde.dumps(metadata)
|
||||
serialized_metadata = self.jsonplus_serde.dumps(
|
||||
get_checkpoint_metadata(config, metadata)
|
||||
)
|
||||
with self.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
|
||||
@@ -16,6 +16,7 @@ from langgraph.checkpoint.base import (
|
||||
CheckpointTuple,
|
||||
SerializerProtocol,
|
||||
get_checkpoint_id,
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
from langgraph.checkpoint.serde.types import ChannelProtocol
|
||||
@@ -463,7 +464,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
||||
thread_id = config["configurable"]["thread_id"]
|
||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
||||
serialized_metadata = self.jsonplus_serde.dumps(metadata)
|
||||
serialized_metadata = self.jsonplus_serde.dumps(
|
||||
get_checkpoint_metadata(config, metadata)
|
||||
)
|
||||
async with (
|
||||
self.lock,
|
||||
self.conn.execute(
|
||||
|
||||
Generated
+2
-2
@@ -350,7 +350,7 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.10"
|
||||
version = "2.0.15"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -1043,4 +1043,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.9.0"
|
||||
content-hash = "03c697eae6f550f3c7e29f1d61f4c409dabe04ae8d43281728e549174d2fc670"
|
||||
content-hash = "e6d3ca9bce723c05f4c5ae9dc4bee872f7581b7763680b34112f1d280f5a9b0a"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "2.0.3"
|
||||
version = "2.0.5"
|
||||
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0"
|
||||
langgraph-checkpoint = "^2.0.10"
|
||||
langgraph-checkpoint = "^2.0.15"
|
||||
aiosqlite = "^0.20.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
@@ -57,6 +57,24 @@ class TestAsyncSqliteSaver:
|
||||
}
|
||||
self.metadata_3: CheckpointMetadata = {}
|
||||
|
||||
async def test_combined_metadata(self) -> None:
|
||||
async with AsyncSqliteSaver.from_conn_string(":memory:") as saver:
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "thread-2",
|
||||
"checkpoint_ns": "",
|
||||
"__super_private_key": "super_private_value",
|
||||
},
|
||||
"metadata": {"run_id": "my_run_id"},
|
||||
}
|
||||
await saver.aput(config, self.chkpnt_2, self.metadata_2, {})
|
||||
checkpoint = await saver.aget_tuple(config)
|
||||
assert checkpoint.metadata == {
|
||||
**self.metadata_2,
|
||||
"thread_id": "thread-2",
|
||||
"run_id": "my_run_id",
|
||||
}
|
||||
|
||||
async def test_asearch(self) -> None:
|
||||
async with AsyncSqliteSaver.from_conn_string(":memory:") as saver:
|
||||
await saver.aput(self.config_1, self.chkpnt_1, self.metadata_1, {})
|
||||
@@ -74,11 +92,18 @@ class TestAsyncSqliteSaver:
|
||||
|
||||
search_results_1 = [c async for c in saver.alist(None, filter=query_1)]
|
||||
assert len(search_results_1) == 1
|
||||
assert search_results_1[0].metadata == self.metadata_1
|
||||
assert search_results_1[0].metadata == {
|
||||
"thread_id": "thread-1",
|
||||
"thread_ts": "1",
|
||||
**self.metadata_1,
|
||||
}
|
||||
|
||||
search_results_2 = [c async for c in saver.alist(None, filter=query_2)]
|
||||
assert len(search_results_2) == 1
|
||||
assert search_results_2[0].metadata == self.metadata_2
|
||||
assert search_results_2[0].metadata == {
|
||||
"thread_id": "thread-2",
|
||||
**self.metadata_2,
|
||||
}
|
||||
|
||||
search_results_3 = [c async for c in saver.alist(None, filter=query_3)]
|
||||
assert len(search_results_3) == 3
|
||||
|
||||
@@ -58,6 +58,24 @@ class TestSqliteSaver:
|
||||
}
|
||||
self.metadata_3: CheckpointMetadata = {}
|
||||
|
||||
def test_combined_metadata(self) -> None:
|
||||
with SqliteSaver.from_conn_string(":memory:") as saver:
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "thread-2",
|
||||
"checkpoint_ns": "",
|
||||
"__super_private_key": "super_private_value",
|
||||
},
|
||||
"metadata": {"run_id": "my_run_id"},
|
||||
}
|
||||
saver.put(config, self.chkpnt_2, self.metadata_2, {})
|
||||
checkpoint = saver.get_tuple(config)
|
||||
assert checkpoint.metadata == {
|
||||
**self.metadata_2,
|
||||
"thread_id": "thread-2",
|
||||
"run_id": "my_run_id",
|
||||
}
|
||||
|
||||
def test_search(self) -> None:
|
||||
with SqliteSaver.from_conn_string(":memory:") as saver:
|
||||
# set up test
|
||||
@@ -77,11 +95,18 @@ class TestSqliteSaver:
|
||||
|
||||
search_results_1 = list(saver.list(None, filter=query_1))
|
||||
assert len(search_results_1) == 1
|
||||
assert search_results_1[0].metadata == self.metadata_1
|
||||
assert search_results_1[0].metadata == {
|
||||
"thread_id": "thread-1",
|
||||
"thread_ts": "1",
|
||||
**self.metadata_1,
|
||||
}
|
||||
|
||||
search_results_2 = list(saver.list(None, filter=query_2))
|
||||
assert len(search_results_2) == 1
|
||||
assert search_results_2[0].metadata == self.metadata_2
|
||||
assert search_results_2[0].metadata == {
|
||||
"thread_id": "thread-2",
|
||||
**self.metadata_2,
|
||||
}
|
||||
|
||||
search_results_3 = list(saver.list(None, filter=query_3))
|
||||
assert len(search_results_3) == 3
|
||||
|
||||
@@ -446,6 +446,23 @@ def get_checkpoint_id(config: RunnableConfig) -> Optional[str]:
|
||||
)
|
||||
|
||||
|
||||
def get_checkpoint_metadata(
|
||||
config: RunnableConfig, metadata: CheckpointMetadata
|
||||
) -> CheckpointMetadata:
|
||||
"""Get checkpoint metadata in a backwards-compatible manner."""
|
||||
metadata = metadata.copy()
|
||||
for obj in (config.get("metadata"), config.get("configurable")):
|
||||
if not obj:
|
||||
continue
|
||||
for key in obj:
|
||||
if key in metadata or key in EXCLUDED_METADATA_KEYS or key.startswith("__"):
|
||||
continue
|
||||
v = obj[key]
|
||||
if isinstance(v, (str, int, bool, float)):
|
||||
metadata[key] = v # type: ignore[literal-required]
|
||||
return metadata
|
||||
|
||||
|
||||
"""
|
||||
Mapping from error type to error index.
|
||||
Regular writes just map to their index in the list of writes being saved.
|
||||
@@ -454,3 +471,9 @@ conflicting with regular writes.
|
||||
Each Checkpointer implementation should use this mapping in put_writes.
|
||||
"""
|
||||
WRITES_IDX_MAP = {ERROR: -1, SCHEDULED: -2, INTERRUPT: -3, RESUME: -4}
|
||||
|
||||
EXCLUDED_METADATA_KEYS = {
|
||||
"checkpoint_id",
|
||||
"checkpoint_ns",
|
||||
"checkpoint_map",
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ from langgraph.checkpoint.base import (
|
||||
CheckpointTuple,
|
||||
SerializerProtocol,
|
||||
get_checkpoint_id,
|
||||
get_checkpoint_metadata,
|
||||
)
|
||||
from langgraph.checkpoint.serde.types import TASKS, ChannelProtocol
|
||||
|
||||
@@ -356,7 +357,7 @@ class InMemorySaver(
|
||||
{
|
||||
checkpoint["id"]: (
|
||||
self.serde.dumps_typed(c),
|
||||
self.serde.dumps_typed(metadata),
|
||||
self.serde.dumps_typed(get_checkpoint_metadata(config, metadata)),
|
||||
config["configurable"].get("checkpoint_id"), # parent
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import json
|
||||
import pathlib
|
||||
import re
|
||||
from collections import deque
|
||||
from collections.abc import Sequence
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
from enum import Enum
|
||||
from inspect import isclass
|
||||
@@ -16,7 +17,7 @@ from ipaddress import (
|
||||
IPv6Interface,
|
||||
IPv6Network,
|
||||
)
|
||||
from typing import Any, Callable, Optional, Sequence, Union, cast
|
||||
from typing import Any, Callable, Optional, Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
import msgpack # type: ignore[import-untyped]
|
||||
@@ -502,15 +503,5 @@ def _msgpack_ext_hook(code: int, data: bytes) -> Any:
|
||||
return
|
||||
|
||||
|
||||
ENC_POOL: deque[msgpack.Packer] = deque(maxlen=32)
|
||||
|
||||
|
||||
def _msgpack_enc(data: Any) -> bytes:
|
||||
try:
|
||||
enc = ENC_POOL.popleft()
|
||||
except IndexError:
|
||||
enc = msgpack.Packer(default=_msgpack_default)
|
||||
try:
|
||||
return enc.pack(data)
|
||||
finally:
|
||||
ENC_POOL.append(enc)
|
||||
return msgpack.packb(data, default=_msgpack_default)
|
||||
|
||||
@@ -493,13 +493,14 @@ class IndexConfig(TypedDict, total=False):
|
||||
- cohere:embed-multilingual-light-v3.0: 384
|
||||
"""
|
||||
|
||||
embed: Union[Embeddings, EmbeddingsFunc, AEmbeddingsFunc]
|
||||
embed: Union[Embeddings, EmbeddingsFunc, AEmbeddingsFunc, str]
|
||||
"""Optional function to generate embeddings from text.
|
||||
|
||||
Can be specified in three ways:
|
||||
1. A LangChain Embeddings instance
|
||||
2. A synchronous embedding function (EmbeddingsFunc)
|
||||
3. An asynchronous embedding function (AEmbeddingsFunc)
|
||||
4. A provider string (e.g., "openai:text-embedding-3-small")
|
||||
|
||||
???+ example "Examples"
|
||||
Using LangChain's initialization with InMemoryStore:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import asyncio
|
||||
import functools
|
||||
import weakref
|
||||
from typing import Any, Callable, Iterable, Literal, Optional, TypeVar, Union
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, Callable, Literal, Optional, TypeVar, Union
|
||||
|
||||
from langgraph.store.base import (
|
||||
BaseStore,
|
||||
@@ -54,19 +55,23 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._loop = asyncio.get_running_loop()
|
||||
self._aqueue: dict[asyncio.Future, Op] = {}
|
||||
self._aqueue: asyncio.Queue[tuple[asyncio.Future, Op]] = asyncio.Queue()
|
||||
self._task = self._loop.create_task(_run(self._aqueue, weakref.ref(self)))
|
||||
|
||||
def __del__(self) -> None:
|
||||
self._task.cancel()
|
||||
try:
|
||||
self._task.cancel()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
async def aget(
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
) -> Optional[Item]:
|
||||
assert not self._task.done()
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue[fut] = GetOp(namespace, key)
|
||||
self._aqueue.put_nowait((fut, GetOp(namespace, key)))
|
||||
return await fut
|
||||
|
||||
async def asearch(
|
||||
@@ -79,8 +84,11 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
) -> list[SearchItem]:
|
||||
assert not self._task.done()
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue[fut] = SearchOp(namespace_prefix, filter, limit, offset, query)
|
||||
self._aqueue.put_nowait(
|
||||
(fut, SearchOp(namespace_prefix, filter, limit, offset, query))
|
||||
)
|
||||
return await fut
|
||||
|
||||
async def aput(
|
||||
@@ -90,9 +98,10 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
value: dict[str, Any],
|
||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
||||
) -> None:
|
||||
assert not self._task.done()
|
||||
_validate_namespace(namespace)
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue[fut] = PutOp(namespace, key, value, index)
|
||||
self._aqueue.put_nowait((fut, PutOp(namespace, key, value, index)))
|
||||
return await fut
|
||||
|
||||
async def adelete(
|
||||
@@ -100,8 +109,9 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
) -> None:
|
||||
assert not self._task.done()
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue[fut] = PutOp(namespace, key, None)
|
||||
self._aqueue.put_nowait((fut, PutOp(namespace, key, None)))
|
||||
return await fut
|
||||
|
||||
async def alist_namespaces(
|
||||
@@ -113,6 +123,7 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> list[tuple[str, ...]]:
|
||||
assert not self._task.done()
|
||||
fut = self._loop.create_future()
|
||||
match_conditions = []
|
||||
if prefix:
|
||||
@@ -126,7 +137,7 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
self._aqueue[fut] = op
|
||||
self._aqueue.put_nowait((fut, op))
|
||||
return await fut
|
||||
|
||||
@_check_loop
|
||||
@@ -250,34 +261,38 @@ def _dedupe_ops(values: list[Op]) -> tuple[Optional[list[int]], list[Op]]:
|
||||
|
||||
|
||||
async def _run(
|
||||
aqueue: dict[asyncio.Future, Op],
|
||||
aqueue: asyncio.Queue[tuple[asyncio.Future, Op]],
|
||||
store: weakref.ReferenceType[BaseStore],
|
||||
) -> None:
|
||||
while True:
|
||||
await asyncio.sleep(0)
|
||||
if not aqueue:
|
||||
continue
|
||||
while item := await aqueue.get():
|
||||
# check if store is still alive
|
||||
if s := store():
|
||||
# get the operations to run
|
||||
taken = aqueue.copy()
|
||||
# action each operation
|
||||
try:
|
||||
values = list(taken.values())
|
||||
listen, dedupped = _dedupe_ops(values)
|
||||
results = await s.abatch(dedupped)
|
||||
if listen is not None:
|
||||
results = [results[ix] for ix in listen]
|
||||
# accumulate operations scheduled in same tick
|
||||
items = [item]
|
||||
try:
|
||||
while item := aqueue.get_nowait():
|
||||
items.append(item)
|
||||
except asyncio.QueueEmpty:
|
||||
pass
|
||||
# get the operations to run
|
||||
futs = [item[0] for item in items]
|
||||
values = [item[1] for item in items]
|
||||
# action each operation
|
||||
try:
|
||||
listen, dedupped = _dedupe_ops(values)
|
||||
results = await s.abatch(dedupped)
|
||||
if listen is not None:
|
||||
results = [results[ix] for ix in listen]
|
||||
|
||||
# set the results of each operation
|
||||
for fut, result in zip(taken, results):
|
||||
fut.set_result(result)
|
||||
except Exception as e:
|
||||
for fut in taken:
|
||||
fut.set_exception(e)
|
||||
# remove the operations from the queue
|
||||
for fut in taken:
|
||||
del aqueue[fut]
|
||||
# set the results of each operation
|
||||
for fut, result in zip(futs, results):
|
||||
fut.set_result(result)
|
||||
except Exception as e:
|
||||
for fut in futs:
|
||||
fut.set_exception(e)
|
||||
finally:
|
||||
# remove strong ref to store
|
||||
del s
|
||||
else:
|
||||
break
|
||||
# remove strong ref to store
|
||||
del s
|
||||
|
||||
@@ -7,6 +7,7 @@ asynchronous operations.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import json
|
||||
from typing import Any, Awaitable, Callable, Optional, Sequence, Union
|
||||
|
||||
@@ -28,7 +29,7 @@ Similar to EmbeddingsFunc, but returns an awaitable that resolves to the embeddi
|
||||
|
||||
|
||||
def ensure_embeddings(
|
||||
embed: Union[Embeddings, EmbeddingsFunc, AEmbeddingsFunc, None],
|
||||
embed: Union[Embeddings, EmbeddingsFunc, AEmbeddingsFunc, str, None],
|
||||
) -> Embeddings:
|
||||
"""Ensure that an embedding function conforms to LangChain's Embeddings interface.
|
||||
|
||||
@@ -62,9 +63,37 @@ def ensure_embeddings(
|
||||
embeddings = ensure_embeddings(my_async_fn)
|
||||
result = await embeddings.aembed_query("hello") # Returns [0.1, 0.2]
|
||||
```
|
||||
|
||||
Initialize embeddings using a provider string:
|
||||
```python
|
||||
# Requires langchain>=0.3.9 and langgraph-checkpoint>=2.0.11
|
||||
embeddings = ensure_embeddings("openai:text-embedding-3-small")
|
||||
result = embeddings.embed_query("hello")
|
||||
```
|
||||
"""
|
||||
if embed is None:
|
||||
raise ValueError("embed must be provided")
|
||||
if isinstance(embed, str):
|
||||
init_embeddings = _get_init_embeddings()
|
||||
if init_embeddings is None:
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
|
||||
try:
|
||||
lc_version = version("langchain")
|
||||
version_info = f"Found langchain version {lc_version}, but"
|
||||
except PackageNotFoundError:
|
||||
version_info = "langchain is not installed;"
|
||||
|
||||
raise ValueError(
|
||||
f"Could not load embeddings from string '{embed}'. {version_info} "
|
||||
"loading embeddings by provider:identifier string requires langchain>=0.3.9 "
|
||||
"as well as the provider-specific package. "
|
||||
"Install LangChain with: pip install 'langchain>=0.3.9' "
|
||||
"and the provider-specific package (e.g., 'langchain-openai>=0.3.0'). "
|
||||
"Alternatively, specify 'embed' as a compatible Embeddings object or python function."
|
||||
)
|
||||
return init_embeddings(embed)
|
||||
|
||||
if isinstance(embed, Embeddings):
|
||||
return embed
|
||||
return EmbeddingsLambda(embed)
|
||||
@@ -373,6 +402,16 @@ def _is_async_callable(
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def _get_init_embeddings() -> Optional[Callable[[str], Embeddings]]:
|
||||
try:
|
||||
from langchain.embeddings import init_embeddings # type: ignore
|
||||
|
||||
return init_embeddings
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ensure_embeddings",
|
||||
"EmbeddingsFunc",
|
||||
|
||||
@@ -493,7 +493,7 @@ def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
|
||||
if not Y:
|
||||
return []
|
||||
if _check_numpy():
|
||||
import numpy as np # type: ignore
|
||||
import numpy as np # type: ignore[import-not-found]
|
||||
|
||||
X_arr = np.array(X) if not isinstance(X, np.ndarray) else X
|
||||
Y_arr = np.array(Y) if not isinstance(Y, np.ndarray) else Y
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.11"
|
||||
version = "2.0.16"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -59,6 +59,24 @@ class TestMemorySaver:
|
||||
}
|
||||
self.metadata_3: CheckpointMetadata = {}
|
||||
|
||||
def test_combined_metadata(self) -> None:
|
||||
config: RunnableConfig = {
|
||||
"configurable": {
|
||||
"thread_id": "thread-2",
|
||||
"checkpoint_ns": "",
|
||||
"__super_private_key": "super_private_value",
|
||||
},
|
||||
"metadata": {"run_id": "my_run_id"},
|
||||
}
|
||||
self.memory_saver.put(config, self.chkpnt_2, self.metadata_2, {})
|
||||
checkpoint = self.memory_saver.get_tuple(config)
|
||||
assert checkpoint is not None
|
||||
assert checkpoint.metadata == {
|
||||
**self.metadata_2,
|
||||
"thread_id": "thread-2",
|
||||
"run_id": "my_run_id",
|
||||
}
|
||||
|
||||
async def test_search(self) -> None:
|
||||
# set up test
|
||||
# save checkpoints
|
||||
@@ -77,11 +95,18 @@ class TestMemorySaver:
|
||||
|
||||
search_results_1 = list(self.memory_saver.list(None, filter=query_1))
|
||||
assert len(search_results_1) == 1
|
||||
assert search_results_1[0].metadata == self.metadata_1
|
||||
assert search_results_1[0].metadata == {
|
||||
"thread_id": "thread-1",
|
||||
"thread_ts": "1",
|
||||
**self.metadata_1,
|
||||
}
|
||||
|
||||
search_results_2 = list(self.memory_saver.list(None, filter=query_2))
|
||||
assert len(search_results_2) == 1
|
||||
assert search_results_2[0].metadata == self.metadata_2
|
||||
assert search_results_2[0].metadata == {
|
||||
"thread_id": "thread-2",
|
||||
**self.metadata_2,
|
||||
}
|
||||
|
||||
search_results_3 = list(self.memory_saver.list(None, filter=query_3))
|
||||
assert len(search_results_3) == 3
|
||||
@@ -121,13 +146,20 @@ class TestMemorySaver:
|
||||
c async for c in self.memory_saver.alist(None, filter=query_1)
|
||||
]
|
||||
assert len(search_results_1) == 1
|
||||
assert search_results_1[0].metadata == self.metadata_1
|
||||
assert search_results_1[0].metadata == {
|
||||
"thread_id": "thread-1",
|
||||
"thread_ts": "1",
|
||||
**self.metadata_1,
|
||||
}
|
||||
|
||||
search_results_2 = [
|
||||
c async for c in self.memory_saver.alist(None, filter=query_2)
|
||||
]
|
||||
assert len(search_results_2) == 1
|
||||
assert search_results_2[0].metadata == self.metadata_2
|
||||
assert search_results_2[0].metadata == {
|
||||
"thread_id": "thread-2",
|
||||
**self.metadata_2,
|
||||
}
|
||||
|
||||
search_results_3 = [
|
||||
c async for c in self.memory_saver.alist(None, filter=query_3)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from contextvars import ContextVar
|
||||
from typing import Any
|
||||
|
||||
from starlette.applications import Starlette
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.routing import Route
|
||||
|
||||
my_context_var: ContextVar[str] = ContextVar("my_context_var", default="")
|
||||
LIFESPAN_VAL = ""
|
||||
other_context_var = ContextVar("other_context_var", default="")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def my_lifespan(app):
|
||||
global LIFESPAN_VAL
|
||||
LIFESPAN_VAL = "foobar-lifespan"
|
||||
yield
|
||||
assert LIFESPAN_VAL == "foobar-lifespan"
|
||||
LIFESPAN_VAL = ""
|
||||
|
||||
|
||||
class MyContextMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Any, call_next: Any) -> Any:
|
||||
token = my_context_var.set("Foobar")
|
||||
try:
|
||||
response = await call_next(request)
|
||||
return response
|
||||
finally:
|
||||
my_context_var.reset(token)
|
||||
|
||||
|
||||
async def custom_my_route(request):
|
||||
"""A great route."""
|
||||
assert my_context_var.get() == "Foobar"
|
||||
assert LIFESPAN_VAL == "foobar-lifespan"
|
||||
return JSONResponse({"foo": "bar"})
|
||||
|
||||
|
||||
async def runs_afakeroute(request):
|
||||
"""Another great route."""
|
||||
assert my_context_var.get() == "Foobar"
|
||||
assert LIFESPAN_VAL == "foobar-lifespan"
|
||||
return JSONResponse({"foo": "afakeroute"})
|
||||
|
||||
|
||||
async def other_middleware(request: Any, call_next: Any) -> Any:
|
||||
other_context_var.set("foobar")
|
||||
response = await call_next(request)
|
||||
other_context_var.reset()
|
||||
return response
|
||||
|
||||
|
||||
app = Starlette(
|
||||
middleware=[(MyContextMiddleware, {}, {})],
|
||||
routes=[
|
||||
Route("/custom/my-route", custom_my_route),
|
||||
Route("/runs/afakeroute", runs_afakeroute),
|
||||
],
|
||||
lifespan=my_lifespan,
|
||||
)
|
||||
@@ -303,7 +303,15 @@ def _build(
|
||||
tag,
|
||||
]
|
||||
# apply config
|
||||
stdin = langgraph_cli.config.config_to_docker(config, config_json, base_image)
|
||||
stdin, additional_contexts = langgraph_cli.config.config_to_docker(
|
||||
config, config_json, base_image
|
||||
)
|
||||
# add additional_contexts
|
||||
if additional_contexts:
|
||||
additional_contexts_str = ",".join(
|
||||
f"{k}={v}" for k, v in additional_contexts.items()
|
||||
)
|
||||
args.extend(["--build-context", additional_contexts_str])
|
||||
# run docker build
|
||||
runner.run(
|
||||
subp_exec(
|
||||
@@ -439,20 +447,28 @@ def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -
|
||||
secho("✅ Configuration validated!", fg="green")
|
||||
|
||||
secho(f"📝 Generating Dockerfile at {save_path}", fg="yellow")
|
||||
dockerfile, additional_contexts = langgraph_cli.config.config_to_docker(
|
||||
config,
|
||||
config_json,
|
||||
(
|
||||
"langchain/langgraphjs-api"
|
||||
if config_json.get("node_version")
|
||||
else "langchain/langgraph-api"
|
||||
),
|
||||
)
|
||||
with open(str(save_path), "w", encoding="utf-8") as f:
|
||||
f.write(
|
||||
langgraph_cli.config.config_to_docker(
|
||||
config,
|
||||
config_json,
|
||||
(
|
||||
"langchain/langgraphjs-api"
|
||||
if config_json.get("node_version")
|
||||
else "langchain/langgraph-api"
|
||||
),
|
||||
)
|
||||
)
|
||||
f.write(dockerfile)
|
||||
secho("✅ Created: Dockerfile", fg="green")
|
||||
|
||||
if additional_contexts:
|
||||
additional_contexts_str = ",".join(
|
||||
f"{k}={v}" for k, v in additional_contexts.items()
|
||||
)
|
||||
secho(
|
||||
f"""📝 Run docker build with these additional build contexts `--build-context {additional_contexts_str}`""",
|
||||
fg="yellow",
|
||||
)
|
||||
|
||||
if add_docker_compose:
|
||||
# Add docker compose and related files
|
||||
# Add .dockerignore file in the same directory as the Dockerfile
|
||||
@@ -575,7 +591,7 @@ def dev(
|
||||
):
|
||||
"""CLI entrypoint for running the LangGraph API server."""
|
||||
try:
|
||||
from langgraph_api.cli import run_server
|
||||
from langgraph_api.cli import run_server # type: ignore
|
||||
except ImportError:
|
||||
py_version_msg = ""
|
||||
if sys.version_info < (3, 11):
|
||||
@@ -634,6 +650,7 @@ def dev(
|
||||
store=config_json.get("store"),
|
||||
wait_for_client=wait_for_client,
|
||||
auth=config_json.get("auth"),
|
||||
http=config_json.get("http"),
|
||||
)
|
||||
|
||||
|
||||
@@ -662,6 +679,7 @@ def prepare_args_and_stdin(
|
||||
debugger_base_url: Optional[str] = None,
|
||||
postgres_uri: Optional[str] = None,
|
||||
) -> Tuple[List[str], str]:
|
||||
assert config_path.exists(), f"Config file not found: {config_path}"
|
||||
# prepare args
|
||||
stdin = langgraph_cli.docker.compose(
|
||||
capabilities,
|
||||
|
||||
@@ -2,6 +2,7 @@ import json
|
||||
import os
|
||||
import pathlib
|
||||
import textwrap
|
||||
from collections import Counter
|
||||
from typing import NamedTuple, Optional, TypedDict, Union
|
||||
|
||||
import click
|
||||
@@ -85,6 +86,33 @@ class AuthConfig(TypedDict, total=False):
|
||||
"""
|
||||
|
||||
|
||||
class CorsConfig(TypedDict, total=False):
|
||||
allow_origins: list[str]
|
||||
allow_methods: list[str]
|
||||
allow_headers: list[str]
|
||||
allow_credentials: bool
|
||||
allow_origin_regex: str
|
||||
expose_headers: list[str]
|
||||
max_age: int
|
||||
|
||||
|
||||
class HttpConfig(TypedDict, total=False):
|
||||
app: str
|
||||
"""Import path for a custom Starlette/FastAPI app to mount"""
|
||||
disable_assistants: bool
|
||||
"""Disable /assistants routes"""
|
||||
disable_threads: bool
|
||||
"""Disable /threads routes"""
|
||||
disable_runs: bool
|
||||
"""Disable /runs routes"""
|
||||
disable_store: bool
|
||||
"""Disable /store routes"""
|
||||
disable_meta: bool
|
||||
"""Disable /ok, /info, /metrics, and /docs routes"""
|
||||
cors: Optional[CorsConfig]
|
||||
"""Cross-Origin Resource Sharing (CORS) configuration"""
|
||||
|
||||
|
||||
class Config(TypedDict, total=False):
|
||||
"""Configuration for langgraph-cli."""
|
||||
|
||||
@@ -123,6 +151,9 @@ class Config(TypedDict, total=False):
|
||||
auth: Optional[AuthConfig]
|
||||
"""Configuration for authentication."""
|
||||
|
||||
http: Optional[HttpConfig]
|
||||
"""Configuration for HTTP server."""
|
||||
|
||||
|
||||
def _parse_version(version_str: str) -> tuple[int, int]:
|
||||
"""Parse a version string into a tuple of (major, minor)."""
|
||||
@@ -157,6 +188,7 @@ def validate_config(config: Config) -> Config:
|
||||
"env": config.get("env", {}),
|
||||
"store": config.get("store"),
|
||||
"auth": config.get("auth"),
|
||||
"http": config.get("http"),
|
||||
}
|
||||
if config.get("node_version")
|
||||
else {
|
||||
@@ -168,6 +200,7 @@ def validate_config(config: Config) -> Config:
|
||||
"env": config.get("env", {}),
|
||||
"store": config.get("store"),
|
||||
"auth": config.get("auth"),
|
||||
"http": config.get("http"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -220,7 +253,13 @@ def validate_config(config: Config) -> Config:
|
||||
f"Invalid auth.path format: '{auth_conf['path']}'. "
|
||||
"Must be in format './path/to/file.py:attribute_name'"
|
||||
)
|
||||
|
||||
if http_conf := config.get("http"):
|
||||
if "app" in http_conf:
|
||||
if ":" not in http_conf["app"]:
|
||||
raise ValueError(
|
||||
f"Invalid http.app format: '{http_conf['app']}'. "
|
||||
"Must be in format './path/to/file.py:attribute_name'"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@@ -294,10 +333,10 @@ class LocalDeps(NamedTuple):
|
||||
tuples. Each entry points to a local `requirements.txt` file and where
|
||||
it should be placed inside the Docker container before running `pip install`.
|
||||
|
||||
real_pkgs: A dictionary mapping a local directory path (host side) to the
|
||||
same dependency string from the config. These directories contain the
|
||||
necessary files (e.g., `pyproject.toml` or `setup.py`) to be installed
|
||||
as a standard Python package with pip.
|
||||
real_pkgs: A dictionary mapping a local directory path (host side) to a
|
||||
tuple of (dependency_string, container_package_path). These directories
|
||||
contain the necessary files (e.g., `pyproject.toml` or `setup.py`) to be
|
||||
installed as a standard Python package with pip.
|
||||
|
||||
faux_pkgs: A dictionary mapping a local directory path (host side) to a
|
||||
tuple of (dependency_string, container_package_path). For these
|
||||
@@ -310,16 +349,23 @@ class LocalDeps(NamedTuple):
|
||||
directory. If the local dependency `"."` is present in the config, this
|
||||
field captures the path where that dependency will appear in the
|
||||
container (e.g., `/deps/<name>` or similar). Otherwise, it may be `None`.
|
||||
|
||||
additional_contexts: A list of paths to directories that contain local
|
||||
dependencies in parent directories. These directories are added to the
|
||||
Docker build context to ensure that the Dockerfile can access them.
|
||||
"""
|
||||
|
||||
pip_reqs: list[tuple[str, str]]
|
||||
real_pkgs: dict[pathlib.Path, str]
|
||||
pip_reqs: list[tuple[pathlib.Path, str]]
|
||||
real_pkgs: dict[pathlib.Path, tuple[str, str]]
|
||||
faux_pkgs: dict[pathlib.Path, tuple[str, str]]
|
||||
# if . is in dependencies, use it as working_dir
|
||||
working_dir: Optional[str] = None
|
||||
# if there are local dependencies in parent directories, use additional_contexts
|
||||
additional_contexts: list[pathlib.Path] = None
|
||||
|
||||
|
||||
def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps:
|
||||
config_path = config_path.resolve()
|
||||
# ensure reserved package names are not used
|
||||
reserved = {
|
||||
"src",
|
||||
@@ -336,6 +382,7 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
"httpx",
|
||||
"langsmith",
|
||||
}
|
||||
counter = Counter()
|
||||
|
||||
def check_reserved(name: str, ref: str):
|
||||
if name in reserved:
|
||||
@@ -348,7 +395,8 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
pip_reqs = []
|
||||
real_pkgs = {}
|
||||
faux_pkgs = {}
|
||||
working_dir = None
|
||||
working_dir: Optional[str] = None
|
||||
additional_contexts: list[pathlib.Path] = []
|
||||
|
||||
for local_dep in config["dependencies"]:
|
||||
if not local_dep.startswith("."):
|
||||
@@ -357,7 +405,7 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
|
||||
# Verify that the local dependency can be resolved
|
||||
# (e.g., this would raise an informative error if a user mistyped a path).
|
||||
resolved = config_path.parent / local_dep
|
||||
resolved = (config_path.parent / local_dep).resolve()
|
||||
|
||||
# validate local dependency
|
||||
if not resolved.exists():
|
||||
@@ -366,25 +414,28 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
raise NotADirectoryError(
|
||||
f"Local dependency must be a directory: {resolved}"
|
||||
)
|
||||
elif not resolved.is_relative_to(config_path.parent):
|
||||
raise ValueError(
|
||||
f"Local dependency '{resolved}' must be a subdirectory of '{config_path.parent}'"
|
||||
)
|
||||
elif resolved == config_path.parent:
|
||||
pass
|
||||
elif config_path.parent not in resolved.parents:
|
||||
additional_contexts.append(resolved)
|
||||
|
||||
# Check for pyproject.toml or setup.py
|
||||
# If found, treat as a real package, if not treat as a faux package.
|
||||
# For faux packages, we'll also check for presence of requirements.txt.
|
||||
files = os.listdir(resolved)
|
||||
if "pyproject.toml" in files:
|
||||
if "pyproject.toml" in files or "setup.py" in files:
|
||||
# real package
|
||||
real_pkgs[resolved] = local_dep
|
||||
|
||||
# assign a unique folder name
|
||||
container_name = resolved.name
|
||||
if counter[container_name] > 0:
|
||||
container_name += f"_{counter[container_name]}"
|
||||
counter[container_name] += 1
|
||||
# add to deps
|
||||
real_pkgs[resolved] = (local_dep, container_name)
|
||||
# set working_dir
|
||||
if local_dep == ".":
|
||||
working_dir = f"/deps/{resolved.name}"
|
||||
elif "setup.py" in files:
|
||||
# real package
|
||||
real_pkgs[resolved] = local_dep
|
||||
if local_dep == ".":
|
||||
working_dir = f"/deps/{resolved.name}"
|
||||
working_dir = f"/deps/{container_name}"
|
||||
else:
|
||||
# We could not find a pyproject.toml or setup.py, so treat as a faux package
|
||||
if any(file == "__init__.py" for file in files):
|
||||
@@ -423,12 +474,12 @@ def _assemble_local_deps(config_path: pathlib.Path, config: Config) -> LocalDeps
|
||||
rfile = resolved / "requirements.txt"
|
||||
pip_reqs.append(
|
||||
(
|
||||
rfile.relative_to(config_path.parent).as_posix(),
|
||||
rfile,
|
||||
f"{container_path}/requirements.txt",
|
||||
)
|
||||
)
|
||||
|
||||
return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir)
|
||||
return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir, additional_contexts)
|
||||
|
||||
|
||||
def _update_graph_paths(
|
||||
@@ -554,9 +605,62 @@ def _update_auth_path(
|
||||
)
|
||||
|
||||
|
||||
def _update_http_app_path(
|
||||
config_path: pathlib.Path, config: Config, local_deps: LocalDeps
|
||||
) -> None:
|
||||
"""Update the HTTP app path to point to the correct location in the Docker container.
|
||||
|
||||
Similar to _update_graph_paths, this ensures that if a custom app is specified via
|
||||
a local file path, that file is included in the Docker build context and its path
|
||||
is updated to point to the correct location in the container.
|
||||
"""
|
||||
if not (http_config := config.get("http")) or not (
|
||||
app_str := http_config.get("app")
|
||||
):
|
||||
return
|
||||
|
||||
module_str, _, attr_str = app_str.partition(":")
|
||||
if not module_str or not attr_str:
|
||||
message = (
|
||||
'Import string "{import_str}" must be in format "<module>:<attribute>".'
|
||||
)
|
||||
raise ValueError(message.format(import_str=app_str))
|
||||
|
||||
# Check if it's a file path
|
||||
if "/" in module_str or "\\" in module_str:
|
||||
# Resolve the local path properly on the current OS
|
||||
resolved = (config_path.parent / module_str).resolve()
|
||||
if not resolved.exists():
|
||||
raise FileNotFoundError(f"Could not find HTTP app module: {resolved}")
|
||||
elif not resolved.is_file():
|
||||
raise IsADirectoryError(f"HTTP app module must be a file: {resolved}")
|
||||
else:
|
||||
for path in local_deps.real_pkgs:
|
||||
if resolved.is_relative_to(path):
|
||||
container_path = (
|
||||
pathlib.Path("/deps") / path.name / resolved.relative_to(path)
|
||||
)
|
||||
module_str = container_path.as_posix()
|
||||
break
|
||||
else:
|
||||
for faux_pkg, (_, destpath) in local_deps.faux_pkgs.items():
|
||||
if resolved.is_relative_to(faux_pkg):
|
||||
container_subpath = resolved.relative_to(faux_pkg)
|
||||
# Construct the final path, ensuring POSIX style
|
||||
module_str = f"{destpath}/{container_subpath.as_posix()}"
|
||||
break
|
||||
else:
|
||||
raise ValueError(
|
||||
f"HTTP app module '{app_str}' not found in 'dependencies' list. "
|
||||
"Add its containing package to 'dependencies' list."
|
||||
)
|
||||
# update the config
|
||||
http_config["app"] = f"{module_str}:{attr_str}"
|
||||
|
||||
|
||||
def python_config_to_docker(
|
||||
config_path: pathlib.Path, config: Config, base_image: str
|
||||
) -> str:
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
"""Generate a Dockerfile from the configuration."""
|
||||
# configure pip
|
||||
pip_install = (
|
||||
@@ -577,13 +681,21 @@ def python_config_to_docker(
|
||||
_update_graph_paths(config_path, config, local_deps)
|
||||
# Rewrite auth path, so it points to the correct location in the Docker container
|
||||
_update_auth_path(config_path, config, local_deps)
|
||||
# Rewrite HTTP app path, so it points to the correct location in the Docker container
|
||||
_update_http_app_path(config_path, config, local_deps)
|
||||
|
||||
pip_pkgs_str = f"RUN {pip_install} {' '.join(pypi_deps)}" if pypi_deps else ""
|
||||
if local_deps.pip_reqs:
|
||||
pip_reqs_str = os.linesep.join(
|
||||
f"ADD {reqpath} {destpath}" for reqpath, destpath in local_deps.pip_reqs
|
||||
f"COPY --from=__outer_{reqpath.name} requirements.txt {destpath}"
|
||||
if reqpath.parent in local_deps.additional_contexts
|
||||
else f"ADD {reqpath.relative_to(config_path.parent)} {destpath}"
|
||||
for reqpath, destpath in local_deps.pip_reqs
|
||||
)
|
||||
pip_reqs_str += f'{os.linesep}RUN {pip_install} {" ".join("-r " + r for _,r in local_deps.pip_reqs)}'
|
||||
pip_reqs_str = f"""# -- Installing local requirements --
|
||||
{pip_reqs_str}
|
||||
# -- End of local requirements install --"""
|
||||
|
||||
else:
|
||||
pip_reqs_str = ""
|
||||
@@ -591,7 +703,14 @@ def python_config_to_docker(
|
||||
# https://setuptools.pypa.io/en/latest/userguide/datafiles.html#package-data
|
||||
# https://til.simonwillison.net/python/pyproject
|
||||
faux_pkgs_str = f"{os.linesep}{os.linesep}".join(
|
||||
f"""ADD {relpath} {destpath}
|
||||
(
|
||||
f"""# -- Adding non-package dependency {fullpath.name} --
|
||||
COPY --from=__outer_{fullpath.name} . {destpath}"""
|
||||
if fullpath in local_deps.additional_contexts
|
||||
else f"""# -- Adding non-package dependency {fullpath.name} --
|
||||
ADD {relpath} {destpath}"""
|
||||
)
|
||||
+ f"""
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
'name = "{fullpath.name}"' \\
|
||||
@@ -599,12 +718,20 @@ RUN set -ex && \\
|
||||
'[tool.setuptools.package-data]' \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_{fullpath.name}/pyproject.toml; \\
|
||||
done"""
|
||||
done
|
||||
# -- End of non-package dependency {fullpath.name} --"""
|
||||
for fullpath, (relpath, destpath) in local_deps.faux_pkgs.items()
|
||||
)
|
||||
|
||||
local_pkgs_str = os.linesep.join(
|
||||
f"ADD {relpath} /deps/{fullpath.name}"
|
||||
for fullpath, relpath in local_deps.real_pkgs.items()
|
||||
f"""# -- Adding local package {relpath} --
|
||||
COPY --from={name} . /deps/{name}
|
||||
# -- End of local package {relpath} --"""
|
||||
if fullpath in local_deps.additional_contexts
|
||||
else f"""# -- Adding local package {relpath} --
|
||||
ADD {relpath} /deps/{name}
|
||||
# -- End of local package {relpath} --"""
|
||||
for fullpath, (relpath, name) in local_deps.real_pkgs.items()
|
||||
)
|
||||
|
||||
installs = f"{os.linesep}{os.linesep}".join(
|
||||
@@ -628,6 +755,9 @@ RUN set -ex && \\
|
||||
if (auth_config := config.get("auth")) is not None:
|
||||
env_vars.append(f"ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'")
|
||||
|
||||
if (http_config := config.get("http")) is not None:
|
||||
env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
|
||||
|
||||
graphs = config["graphs"]
|
||||
env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(graphs)}'")
|
||||
|
||||
@@ -638,15 +768,30 @@ RUN set -ex && \\
|
||||
"",
|
||||
installs,
|
||||
"",
|
||||
"# -- Installing all local dependencies --",
|
||||
f"RUN {pip_install} -e /deps/*",
|
||||
"# -- End of local dependencies install --",
|
||||
os.linesep.join(env_vars),
|
||||
"",
|
||||
f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "",
|
||||
]
|
||||
return os.linesep.join(docker_file_contents)
|
||||
|
||||
additional_contexts: dict[str, str] = {}
|
||||
for p in local_deps.additional_contexts:
|
||||
if p in local_deps.real_pkgs:
|
||||
name = local_deps.real_pkgs[p][1]
|
||||
elif p in local_deps.faux_pkgs:
|
||||
name = f"__outer_{p.name}"
|
||||
else:
|
||||
raise RuntimeError(f"Unknown additional context: {p}")
|
||||
additional_contexts[name] = str(p)
|
||||
|
||||
return os.linesep.join(docker_file_contents), additional_contexts
|
||||
|
||||
|
||||
def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
|
||||
def node_config_to_docker(
|
||||
config_path: pathlib.Path, config: Config, base_image: str
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
faux_path = f"/deps/{config_path.parent.name}"
|
||||
|
||||
def test_file(file_name):
|
||||
@@ -684,9 +829,14 @@ ENV LANGGRAPH_STORE='{json.dumps(store_config)}'
|
||||
if (auth_config := config.get("auth")) is not None:
|
||||
env_additional_config += f"""
|
||||
ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'
|
||||
"""
|
||||
if (http_config := config.get("http")) is not None:
|
||||
env_additional_config += f"""
|
||||
ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'
|
||||
"""
|
||||
|
||||
return f"""FROM {base_image}:{config['node_version']}
|
||||
return (
|
||||
f"""FROM {base_image}:{config['node_version']}
|
||||
|
||||
{os.linesep.join(config["dockerfile_lines"])}
|
||||
|
||||
@@ -698,10 +848,14 @@ ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
|
||||
|
||||
WORKDIR {faux_path}
|
||||
|
||||
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
|
||||
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts""",
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
def config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
|
||||
def config_to_docker(
|
||||
config_path: pathlib.Path, config: Config, base_image: str
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
if config.get("node_version"):
|
||||
return node_config_to_docker(config_path, config, base_image)
|
||||
|
||||
@@ -737,13 +891,24 @@ def config_to_compose(
|
||||
else:
|
||||
watch_str = ""
|
||||
|
||||
dockerfile, additional_contexts = config_to_docker(config_path, config, base_image)
|
||||
|
||||
additional_contexts_str = "\n".join(
|
||||
f" - {name}: {path}"
|
||||
for name, path in additional_contexts.items()
|
||||
)
|
||||
if additional_contexts_str:
|
||||
additional_contexts_str = f"""
|
||||
additional_contexts:
|
||||
{additional_contexts_str}"""
|
||||
|
||||
return f"""
|
||||
{textwrap.indent(env_vars_str, " ")}
|
||||
{env_file_str}
|
||||
pull_policy: build
|
||||
build:
|
||||
context: .
|
||||
context: .{additional_contexts_str}
|
||||
dockerfile_inline: |
|
||||
{textwrap.indent(config_to_docker(config_path, config, base_image), " ")}
|
||||
{textwrap.indent(dockerfile, " ")}
|
||||
{watch_str}
|
||||
"""
|
||||
|
||||
@@ -49,7 +49,9 @@ def check_capabilities(runner) -> DockerCapabilities:
|
||||
raise click.UsageError("Docker not installed") from None
|
||||
|
||||
try:
|
||||
stdout, _ = runner.run(subp_exec("docker", "info", "-f", "json", collect=True))
|
||||
stdout, _ = runner.run(
|
||||
subp_exec("docker", "info", "-f", "{{json .}}", collect=True)
|
||||
)
|
||||
info = json.loads(stdout)
|
||||
except (click.exceptions.Exit, json.JSONDecodeError):
|
||||
raise click.UsageError("Docker not installed or not running") from None
|
||||
|
||||
Generated
+450
-345
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-cli"
|
||||
version = "0.1.71"
|
||||
version = "0.1.72"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -14,7 +14,7 @@ langgraph = "langgraph_cli.cli:cli"
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
click = "^8.1.7"
|
||||
langgraph-api = { version = ">=0.0.12,<0.1.0", optional = true, python = ">=3.11,<4.0" }
|
||||
langgraph-api = { version = ">=0.0.24,<0.1.0", optional = true, python = ">=3.11,<4.0" }
|
||||
python-dotenv = { version = ">=0.8.0", optional = true }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
@@ -40,9 +40,9 @@ def temporary_config_folder(config_content: dict):
|
||||
|
||||
def test_prepare_args_and_stdin() -> None:
|
||||
# this basically serves as an end-to-end test for using config and docker helpers
|
||||
config_path = pathlib.Path("./langgraph.json")
|
||||
config_path = pathlib.Path(__file__).parent / "langgraph.json"
|
||||
config = validate_config(
|
||||
Config(dependencies=["."], graphs={"agent": "agent.py:graph"})
|
||||
Config(dependencies=[".", "../../.."], graphs={"agent": "agent.py:graph"})
|
||||
)
|
||||
port = 8000
|
||||
debugger_port = 8001
|
||||
@@ -61,7 +61,7 @@ def test_prepare_args_and_stdin() -> None:
|
||||
|
||||
expected_args = [
|
||||
"--project-directory",
|
||||
".",
|
||||
str(pathlib.Path(__file__).parent.absolute()),
|
||||
"-f",
|
||||
"custom-docker-compose.yml",
|
||||
"-f",
|
||||
@@ -129,18 +129,29 @@ services:
|
||||
pull_policy: build
|
||||
build:
|
||||
context: .
|
||||
additional_contexts:
|
||||
- cli_1: {str(pathlib.Path(__file__).parent.parent.parent.parent.absolute())}
|
||||
dockerfile_inline: |
|
||||
FROM langchain/langgraph-api:3.11
|
||||
ADD . /deps/
|
||||
# -- Adding local package . --
|
||||
ADD . /deps/cli
|
||||
# -- End of local package . --
|
||||
# -- Adding local package ../../.. --
|
||||
COPY --from=cli_1 . /deps/cli_1
|
||||
# -- End of local package ../../.. --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
|
||||
WORKDIR /deps/
|
||||
WORKDIR /deps/cli
|
||||
|
||||
develop:
|
||||
watch:
|
||||
- path: langgraph.json
|
||||
action: rebuild
|
||||
- path: .
|
||||
action: rebuild
|
||||
- path: ../../..
|
||||
action: rebuild\
|
||||
"""
|
||||
assert actual_args == expected_args
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
|
||||
@entrypoint()
|
||||
def graph(state):
|
||||
return None
|
||||
|
||||
@@ -6,10 +6,14 @@
|
||||
],
|
||||
"dependencies": [
|
||||
"langchain_openai",
|
||||
"starlette",
|
||||
"."
|
||||
],
|
||||
"graphs": {
|
||||
"agent": "graphs/agent.py:graph"
|
||||
},
|
||||
"env": ".env"
|
||||
"env": ".env",
|
||||
"http": {
|
||||
"app": "../../examples/my_app.py:app"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ def test_validate_config():
|
||||
"env": {},
|
||||
"store": None,
|
||||
"auth": None,
|
||||
"http": None,
|
||||
**expected_config,
|
||||
}
|
||||
actual_config = validate_config(expected_config)
|
||||
@@ -50,6 +51,7 @@ def test_validate_config():
|
||||
"env": env,
|
||||
"store": None,
|
||||
"auth": None,
|
||||
"http": None,
|
||||
}
|
||||
actual_config = validate_config(expected_config)
|
||||
assert actual_config == expected_config
|
||||
@@ -108,6 +110,18 @@ def test_validate_config():
|
||||
}
|
||||
)
|
||||
assert config["python_version"] == "3.12-slim"
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Invalid http.app format",
|
||||
):
|
||||
validate_config(
|
||||
{
|
||||
"python_version": "3.12",
|
||||
"dependencies": ["."],
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
"http": {"app": "../../examples/my_app.py"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_validate_config_file():
|
||||
@@ -177,13 +191,27 @@ def test_validate_config_file():
|
||||
# config_to_docker
|
||||
def test_config_to_docker_simple():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
actual_docker_stdin = config_to_docker(
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"dependencies": ["."], "graphs": graphs}),
|
||||
validate_config(
|
||||
{
|
||||
"dependencies": [".", "../../examples/graphs_reqs_a", "../../examples"],
|
||||
"graphs": graphs,
|
||||
"http": {"app": "../../examples/my_app.py:app"},
|
||||
}
|
||||
),
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = """\
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Installing local requirements --
|
||||
COPY --from=__outer_requirements.txt requirements.txt /deps/__outer_graphs_reqs_a/graphs_reqs_a/requirements.txt
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -r /deps/__outer_graphs_reqs_a/graphs_reqs_a/requirements.txt
|
||||
# -- End of local requirements install --
|
||||
# -- Adding local package ../../examples --
|
||||
COPY --from=examples . /deps/examples
|
||||
# -- End of local package ../../examples --
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -193,16 +221,81 @@ RUN set -ex && \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Adding non-package dependency graphs_reqs_a --
|
||||
COPY --from=__outer_graphs_reqs_a . /deps/__outer_graphs_reqs_a/graphs_reqs_a
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
'name = "graphs_reqs_a"' \\
|
||||
'version = "0.1"' \\
|
||||
'[tool.setuptools.package-data]' \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_graphs_reqs_a/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency graphs_reqs_a --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGGRAPH_HTTP='{"app": "/deps/examples/my_app.py:app"}'
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
||||
"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
|
||||
assert additional_contexts == {
|
||||
"__outer_graphs_reqs_a": str(
|
||||
(pathlib.Path(__file__).parent / "../../examples/graphs_reqs_a").resolve()
|
||||
),
|
||||
"examples": str((pathlib.Path(__file__).parent / "../../examples").resolve()),
|
||||
}
|
||||
|
||||
|
||||
def test_config_to_docker_outside_path():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"dependencies": [".", ".."], "graphs": graphs}),
|
||||
"langchain/langgraph-api",
|
||||
)
|
||||
expected_docker_stdin = """\
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
'name = "unit_tests"' \\
|
||||
'version = "0.1"' \\
|
||||
'[tool.setuptools.package-data]' \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Adding non-package dependency tests --
|
||||
COPY --from=__outer_tests . /deps/__outer_tests/tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
'name = "tests"' \\
|
||||
'version = "0.1"' \\
|
||||
'[tool.setuptools.package-data]' \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
||||
"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {
|
||||
"__outer_tests": str(pathlib.Path(__file__).parent.parent.absolute()),
|
||||
}
|
||||
|
||||
|
||||
def test_config_to_docker_pipconfig():
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
actual_docker_stdin = config_to_docker(
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -216,6 +309,7 @@ def test_config_to_docker_pipconfig():
|
||||
expected_docker_stdin = """\
|
||||
FROM langchain/langgraph-api:3.11
|
||||
ADD pipconfig.txt /pipconfig.txt
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -225,11 +319,15 @@ RUN set -ex && \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
||||
"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
def test_config_to_docker_invalid_inputs():
|
||||
@@ -254,7 +352,7 @@ def test_config_to_docker_invalid_inputs():
|
||||
|
||||
def test_config_to_docker_local_deps():
|
||||
graphs = {"agent": "./graphs/agent.py:graph"}
|
||||
actual_docker_stdin = config_to_docker(
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -266,6 +364,7 @@ def test_config_to_docker_local_deps():
|
||||
)
|
||||
expected_docker_stdin = """\
|
||||
FROM langchain/langgraph-api-custom:3.11
|
||||
# -- Adding non-package dependency graphs --
|
||||
ADD ./graphs /deps/__outer_graphs/src
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -275,10 +374,14 @@ RUN set -ex && \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency graphs --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'\
|
||||
"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
def test_config_to_docker_pyproject():
|
||||
@@ -291,7 +394,7 @@ dependencies = ["langchain"]"""
|
||||
f.write(pyproject_str)
|
||||
|
||||
graphs = {"agent": "./graphs/agent.py:graph"}
|
||||
actual_docker_stdin = config_to_docker(
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -303,16 +406,21 @@ dependencies = ["langchain"]"""
|
||||
)
|
||||
os.remove(pyproject_path)
|
||||
expected_docker_stdin = """FROM langchain/langgraph-api:3.11
|
||||
# -- Adding local package . --
|
||||
ADD . /deps/unit_tests
|
||||
# -- End of local package . --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/unit_tests/graphs/agent.py:graph"}'
|
||||
WORKDIR /deps/unit_tests"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
def test_config_to_docker_end_to_end():
|
||||
graphs = {"agent": "./graphs/agent.py:graph"}
|
||||
actual_docker_stdin = config_to_docker(
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -330,6 +438,7 @@ ARG meow
|
||||
ARG foo
|
||||
ADD pipconfig.txt /pipconfig.txt
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain langchain_openai
|
||||
# -- Adding non-package dependency graphs --
|
||||
ADD ./graphs/ /deps/__outer_graphs/src
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -339,15 +448,19 @@ RUN set -ex && \\
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency graphs --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'"""
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
# node.js build used for LangGraph Cloud
|
||||
def test_config_to_docker_nodejs():
|
||||
graphs = {"agent": "./graphs/agent.js:graph"}
|
||||
actual_docker_stdin = config_to_docker(
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
@@ -368,6 +481,7 @@ WORKDIR /deps/unit_tests
|
||||
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
|
||||
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
assert additional_contexts == {}
|
||||
|
||||
|
||||
# config_to_compose
|
||||
@@ -380,6 +494,7 @@ def test_config_to_compose_simple_config():
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -389,7 +504,10 @@ def test_config_to_compose_simple_config():
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||
"""
|
||||
@@ -410,6 +528,7 @@ def test_config_to_compose_env_vars():
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
FROM langchain/langgraph-api-custom:3.11
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -419,7 +538,10 @@ def test_config_to_compose_env_vars():
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||
"""
|
||||
@@ -447,6 +569,7 @@ def test_config_to_compose_env_file():
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -456,7 +579,10 @@ def test_config_to_compose_env_file():
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||
"""
|
||||
@@ -477,6 +603,7 @@ def test_config_to_compose_watch():
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -486,7 +613,10 @@ def test_config_to_compose_watch():
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||
|
||||
@@ -516,6 +646,7 @@ def test_config_to_compose_end_to_end():
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
FROM langchain/langgraph-api:3.11
|
||||
# -- Adding non-package dependency unit_tests --
|
||||
ADD . /deps/__outer_unit_tests/unit_tests
|
||||
RUN set -ex && \\
|
||||
for line in '[project]' \\
|
||||
@@ -525,7 +656,10 @@ def test_config_to_compose_end_to_end():
|
||||
'"*" = ["**/*"]'; do \\
|
||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||
done
|
||||
# -- End of non-package dependency unit_tests --
|
||||
# -- Installing all local dependencies --
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
# -- End of local dependencies install --
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.runnables.config import var_child_runnable_config
|
||||
@@ -9,6 +10,10 @@ from langgraph.store.base import BaseStore
|
||||
from langgraph.types import StreamWriter
|
||||
|
||||
|
||||
def _no_op_stream_writer(c: Any) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def get_config() -> RunnableConfig:
|
||||
if sys.version_info < (3, 11):
|
||||
try:
|
||||
@@ -177,4 +182,4 @@ def get_stream_writer() -> StreamWriter:
|
||||
```
|
||||
"""
|
||||
config = get_config()
|
||||
return config[CONF][CONFIG_KEY_STREAM_WRITER]
|
||||
return config[CONF].get(CONFIG_KEY_STREAM_WRITER, _no_op_stream_writer)
|
||||
|
||||
@@ -81,6 +81,8 @@ CONFIG_KEY_SCRATCHPAD = sys.intern("__pregel_scratchpad")
|
||||
# holds a mutable dict for temporary storage scoped to the current task
|
||||
CONFIG_KEY_PREVIOUS = sys.intern("__pregel_previous")
|
||||
# holds the previous return value from a stateful Pregel graph.
|
||||
CONFIG_KEY_RUNNER_SUBMIT = sys.intern("__pregel_runner_submit")
|
||||
# holds a function that receives tasks from runner, executes them and returns results
|
||||
|
||||
# --- Other constants ---
|
||||
PUSH = sys.intern("__pregel_push")
|
||||
|
||||
@@ -57,9 +57,6 @@ def task(
|
||||
]:
|
||||
"""Define a LangGraph task using the `task` decorator.
|
||||
|
||||
!!! warning "Beta"
|
||||
The Functional API is currently in beta and is subject to change.
|
||||
|
||||
!!! important "Requires python 3.11 or higher for async functions"
|
||||
The `task` decorator supports both sync and async functions. To use async
|
||||
functions, ensure that you are using Python 3.11 or higher.
|
||||
@@ -153,10 +150,6 @@ S = TypeVar("S")
|
||||
class entrypoint:
|
||||
"""Define a LangGraph workflow using the `entrypoint` decorator.
|
||||
|
||||
!!! warning "Beta"
|
||||
The Functional API is currently in beta and is subject to change.
|
||||
|
||||
|
||||
### Function signature
|
||||
|
||||
The decorated function must accept a **single parameter**, which serves as the input
|
||||
|
||||
@@ -47,7 +47,7 @@ logger = logging.getLogger(__name__)
|
||||
class NodeSpec(NamedTuple):
|
||||
runnable: Runnable
|
||||
metadata: Optional[dict[str, Any]] = None
|
||||
ends: Optional[tuple[str, ...]] = EMPTY_SEQ
|
||||
ends: Optional[Union[tuple[str, ...], dict[str, str]]] = EMPTY_SEQ
|
||||
|
||||
|
||||
class Branch(NamedTuple):
|
||||
@@ -625,7 +625,10 @@ class CompiledGraph(Pregel):
|
||||
if branch.then is not None:
|
||||
add_edge(end, branch.then)
|
||||
for key, n in self.builder.nodes.items():
|
||||
if n.ends:
|
||||
if isinstance(n.ends, dict):
|
||||
for end, label in n.ends.items():
|
||||
add_edge(key, end, label, conditional=True)
|
||||
elif isinstance(n.ends, tuple):
|
||||
for end in n.ends:
|
||||
add_edge(key, end, conditional=True)
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ from langgraph.channels.dynamic_barrier_value import DynamicBarrierValue, WaitFo
|
||||
from langgraph.channels.ephemeral_value import EphemeralValue
|
||||
from langgraph.channels.last_value import LastValue
|
||||
from langgraph.channels.named_barrier_value import NamedBarrierValue
|
||||
from langgraph.constants import EMPTY_SEQ, NS_END, NS_SEP, SELF, TAG_HIDDEN
|
||||
from langgraph.constants import EMPTY_SEQ, MISSING, NS_END, NS_SEP, SELF, TAG_HIDDEN
|
||||
from langgraph.errors import (
|
||||
ErrorCode,
|
||||
InvalidUpdateError,
|
||||
@@ -90,7 +90,7 @@ class StateNodeSpec(NamedTuple):
|
||||
metadata: Optional[dict[str, Any]]
|
||||
input: Type[Any]
|
||||
retry_policy: Optional[RetryPolicy]
|
||||
ends: Optional[tuple[str, ...]] = EMPTY_SEQ
|
||||
ends: Optional[Union[tuple[str, ...], dict[str, str]]] = EMPTY_SEQ
|
||||
|
||||
|
||||
class StateGraph(Graph):
|
||||
@@ -230,6 +230,7 @@ class StateGraph(Graph):
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
input: Optional[Type[Any]] = None,
|
||||
retry: Optional[RetryPolicy] = None,
|
||||
destinations: Optional[Union[dict[str, str], tuple[str]]] = None,
|
||||
) -> Self:
|
||||
"""Adds a new node to the state graph.
|
||||
Will take the name of the function/runnable as the node name.
|
||||
@@ -254,6 +255,7 @@ class StateGraph(Graph):
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
input: Optional[Type[Any]] = None,
|
||||
retry: Optional[RetryPolicy] = None,
|
||||
destinations: Optional[Union[dict[str, str], tuple[str]]] = None,
|
||||
) -> Self:
|
||||
"""Adds a new node to the state graph.
|
||||
|
||||
@@ -277,18 +279,23 @@ class StateGraph(Graph):
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
input: Optional[Type[Any]] = None,
|
||||
retry: Optional[RetryPolicy] = None,
|
||||
destinations: Optional[Union[dict[str, str], tuple[str]]] = None,
|
||||
) -> Self:
|
||||
"""Adds a new node to the state graph.
|
||||
|
||||
Will take the name of the function/runnable as the node name.
|
||||
|
||||
Args:
|
||||
node (Union[str, RunnableLike)]: The function or runnable this node will run.
|
||||
node (Union[str, RunnableLike]): The function or runnable this node will run.
|
||||
action (Optional[RunnableLike]): The action associated with the node. (default: None)
|
||||
metadata (Optional[dict[str, Any]]): The metadata associated with the node. (default: None)
|
||||
input (Optional[Type[Any]]): The input schema for the node. (default: the graph's input schema)
|
||||
retry (Optional[RetryPolicy]): The policy for retrying the node. (default: None)
|
||||
|
||||
destinations (Optional[Union[dict[str, str], tuple[str]]]): Destinations that indicate where a node can route to.
|
||||
This is useful for edgeless graphs with nodes that return `Command` objects.
|
||||
If a dict is provided, the keys will be used as the target node names and the values will be used as the labels for the edges.
|
||||
If a tuple is provided, the values will be used as the target node names.
|
||||
NOTE: this is only used for graph rendering and doesn't have any effect on the graph execution.
|
||||
Raises:
|
||||
ValueError: If the key is already being used as a state key.
|
||||
|
||||
@@ -357,7 +364,7 @@ class StateGraph(Graph):
|
||||
f"'{character}' is a reserved character and is not allowed in the node names."
|
||||
)
|
||||
|
||||
ends = EMPTY_SEQ
|
||||
ends: Union[tuple[str, ...], dict[str, str]] = EMPTY_SEQ
|
||||
try:
|
||||
if (
|
||||
isfunction(action)
|
||||
@@ -401,6 +408,10 @@ class StateGraph(Graph):
|
||||
ends = vals
|
||||
except (TypeError, StopIteration):
|
||||
pass
|
||||
|
||||
if destinations is not None:
|
||||
ends = destinations
|
||||
|
||||
if input is not None:
|
||||
self._add_schema(input)
|
||||
self.nodes[cast(str, node)] = StateNodeSpec(
|
||||
@@ -680,10 +691,23 @@ class CompiledStateGraph(CompiledGraph):
|
||||
updates.extend(_get_updates(i) or ())
|
||||
return updates
|
||||
elif get_type_hints(type(input)):
|
||||
# if input is a Pydantic model, only update values
|
||||
# for the keys that have been explicitly set by the users
|
||||
# (this is needed to avoid sending updates for fields with None defaults)
|
||||
output_keys_ = output_keys
|
||||
# Pydantic v2
|
||||
if hasattr(input, "model_fields_set"):
|
||||
output_keys_ = [
|
||||
k for k in output_keys if k in input.model_fields_set
|
||||
]
|
||||
# Pydantic v1
|
||||
elif hasattr(input, "__fields_set__"):
|
||||
output_keys_ = [k for k in output_keys if k in input.__fields_set__]
|
||||
|
||||
return [
|
||||
(k, getattr(input, k))
|
||||
for k in output_keys
|
||||
if getattr(input, k, None) is not None
|
||||
for k in output_keys_
|
||||
if getattr(input, k, MISSING) is not MISSING
|
||||
]
|
||||
else:
|
||||
msg = create_error_message(
|
||||
|
||||
@@ -12,7 +12,11 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
from langchain_core.language_models import BaseChatModel, LanguageModelLike
|
||||
from langchain_core.language_models import (
|
||||
BaseChatModel,
|
||||
LanguageModelInput,
|
||||
LanguageModelLike,
|
||||
)
|
||||
from langchain_core.messages import AIMessage, BaseMessage, SystemMessage, ToolMessage
|
||||
from langchain_core.runnables import (
|
||||
Runnable,
|
||||
@@ -52,6 +56,10 @@ class AgentState(TypedDict):
|
||||
|
||||
remaining_steps: RemainingSteps
|
||||
|
||||
|
||||
class AgentStateWithStructuredResponse(AgentState):
|
||||
"""The state of the agent with a structured response."""
|
||||
|
||||
structured_response: StructuredResponse
|
||||
|
||||
|
||||
@@ -63,15 +71,15 @@ PROMPT_RUNNABLE_NAME = "Prompt"
|
||||
MessagesModifier = Union[
|
||||
SystemMessage,
|
||||
str,
|
||||
Callable[[Sequence[BaseMessage]], Sequence[BaseMessage]],
|
||||
Runnable[Sequence[BaseMessage], Sequence[BaseMessage]],
|
||||
Callable[[Sequence[BaseMessage]], LanguageModelInput],
|
||||
Runnable[Sequence[BaseMessage], LanguageModelInput],
|
||||
]
|
||||
|
||||
Prompt = Union[
|
||||
SystemMessage,
|
||||
str,
|
||||
Callable[[StateSchema], Sequence[BaseMessage]],
|
||||
Runnable[StateSchema, Sequence[BaseMessage]],
|
||||
Callable[[StateSchema], LanguageModelInput],
|
||||
Runnable[StateSchema, LanguageModelInput],
|
||||
]
|
||||
|
||||
|
||||
@@ -599,6 +607,13 @@ def create_react_agent(
|
||||
if missing_keys := required_keys - set(state_schema.__annotations__):
|
||||
raise ValueError(f"Missing required key(s) {missing_keys} in state_schema")
|
||||
|
||||
if state_schema is None:
|
||||
state_schema = (
|
||||
AgentStateWithStructuredResponse
|
||||
if response_format is not None
|
||||
else AgentState
|
||||
)
|
||||
|
||||
if isinstance(tools, ToolExecutor):
|
||||
tool_classes: Sequence[BaseTool] = tools.tools
|
||||
tool_node = ToolNode(tool_classes)
|
||||
@@ -748,7 +763,7 @@ def create_react_agent(
|
||||
|
||||
if not tool_calling_enabled:
|
||||
# Define a new graph
|
||||
workflow = StateGraph(state_schema or AgentState)
|
||||
workflow = StateGraph(state_schema)
|
||||
workflow.add_node("agent", RunnableCallable(call_model, acall_model))
|
||||
workflow.set_entry_point("agent")
|
||||
if response_format is not None:
|
||||
|
||||
@@ -59,6 +59,7 @@ from langgraph.constants import (
|
||||
CONFIG_KEY_NODE_FINISHED,
|
||||
CONFIG_KEY_READ,
|
||||
CONFIG_KEY_RESUMING,
|
||||
CONFIG_KEY_RUNNER_SUBMIT,
|
||||
CONFIG_KEY_SEND,
|
||||
CONFIG_KEY_STORE,
|
||||
CONFIG_KEY_STREAM,
|
||||
@@ -197,6 +198,60 @@ class Channel:
|
||||
|
||||
|
||||
class Pregel(PregelProtocol):
|
||||
"""Pregel manages the runtime behavior for LangGraph applications.
|
||||
|
||||
## Channels
|
||||
|
||||
Channels are used to communicate between chains. Each channel has a value type,
|
||||
an update type, and an update function – which takes a sequence of updates and
|
||||
modifies the stored value. Channels can be used to send data from one chain to
|
||||
another, or to send data from a chain to itself in a future step. LangGraph
|
||||
provides a number of built-in channels:
|
||||
|
||||
### Basic channels: LastValue and Topic
|
||||
|
||||
- `LastValue`: The default channel, stores the last value sent to the channel,
|
||||
useful for input and output values, or for sending data from one step to the next
|
||||
- `Topic`: A configurable PubSub Topic, useful for sending multiple values
|
||||
between chains, or for accumulating output. Can be configured to deduplicate
|
||||
values, and/or to accumulate values over the course of multiple steps.
|
||||
|
||||
### Advanced channels: Context and BinaryOperatorAggregate
|
||||
|
||||
- `Context`: exposes the value of a context manager, managing its lifecycle.
|
||||
Useful for accessing external resources that require setup and/or teardown. eg.
|
||||
`client = Context(httpx.Client)`
|
||||
- `BinaryOperatorAggregate`: stores a persistent value, updated by applying
|
||||
a binary operator to the current value and each update
|
||||
sent to the channel, useful for computing aggregates over multiple steps. eg.
|
||||
`total = BinaryOperatorAggregate(int, operator.add)`
|
||||
|
||||
## Chains
|
||||
|
||||
Chains are LCEL Runnables which subscribe to one or more channels, and write to
|
||||
one or more channels. Any valid LCEL expression can be used as a chain. Chains
|
||||
can be combined into a Pregel application, which coordinates the execution of the
|
||||
chains across multiple steps.
|
||||
|
||||
## Pregel
|
||||
|
||||
Pregel combines multiple chains (or actors) into a single application. It
|
||||
coordinates the execution of the chains across multiple steps, following the
|
||||
Pregel/Bulk Synchronous Parallel model. Each step consists of three phases:
|
||||
|
||||
- **Plan**: Determine which chains to execute in this step, ie. the chains that
|
||||
subscribe to channels updated in the previous step (or, in the first step,
|
||||
chains that subscribe to input channels)
|
||||
- **Execution**: Execute those chains in parallel, until all complete, or one fails,
|
||||
or a timeout is reached. Any channel updates are invisible to other
|
||||
chains until the next step.
|
||||
- **Update**: Update the channels with the values written by the
|
||||
chains in this step.
|
||||
|
||||
Repeat until no chains are planned for execution, or a maximum number of steps
|
||||
is reached.
|
||||
"""
|
||||
|
||||
nodes: dict[str, PregelNode]
|
||||
|
||||
channels: dict[str, Union[BaseChannel, ManagedValueSpec]]
|
||||
@@ -1682,7 +1737,7 @@ class Pregel(PregelProtocol):
|
||||
) as loop:
|
||||
# create runner
|
||||
runner = PregelRunner(
|
||||
submit=loop.submit,
|
||||
submit=config[CONF].get(CONFIG_KEY_RUNNER_SUBMIT, loop.submit),
|
||||
put_writes=loop.put_writes,
|
||||
schedule_task=loop.accept_push,
|
||||
node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
|
||||
@@ -1974,7 +2029,7 @@ class Pregel(PregelProtocol):
|
||||
) as loop:
|
||||
# create runner
|
||||
runner = PregelRunner(
|
||||
submit=loop.submit,
|
||||
submit=config[CONF].get(CONFIG_KEY_RUNNER_SUBMIT, loop.submit),
|
||||
put_writes=loop.put_writes,
|
||||
schedule_task=loop.accept_push,
|
||||
use_astream=do_stream is not None,
|
||||
|
||||
@@ -323,11 +323,15 @@ class RemoteGraph(PregelProtocol):
|
||||
if k not in reserved_configurable_keys and not k.startswith("__pregel_")
|
||||
}
|
||||
|
||||
return {
|
||||
sanitized: RunnableConfig = {
|
||||
"tags": config.get("tags") or [],
|
||||
"metadata": config.get("metadata") or {},
|
||||
"configurable": new_configurable,
|
||||
}
|
||||
if "recursion_limit" in config:
|
||||
sanitized["recursion_limit"] = config["recursion_limit"]
|
||||
|
||||
return sanitized
|
||||
|
||||
def get_state(
|
||||
self, config: RunnableConfig, *, subgraphs: bool = False
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import (
|
||||
Coroutine,
|
||||
Iterator,
|
||||
Optional,
|
||||
Protocol,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Union,
|
||||
@@ -35,7 +36,7 @@ from langchain_core.runnables.config import (
|
||||
)
|
||||
from langchain_core.runnables.utils import Input, Output
|
||||
from langchain_core.tracers._streaming import _StreamingCallbackHandler
|
||||
from typing_extensions import Concatenate, ParamSpec, TypeGuard
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
from langgraph.constants import (
|
||||
CONF,
|
||||
@@ -132,12 +133,51 @@ Each tuple contains:
|
||||
VALID_KINDS = (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
|
||||
|
||||
|
||||
P = ParamSpec("P") # to handle injected kwargs like `writer` / `store`
|
||||
class _RunnableWithWriter(Protocol[Input, Output]):
|
||||
def __call__(self, state: Input, *, writer: StreamWriter) -> Output: ...
|
||||
|
||||
|
||||
class _RunnableWithStore(Protocol[Input, Output]):
|
||||
def __call__(self, state: Input, *, store: BaseStore) -> Output: ...
|
||||
|
||||
|
||||
class _RunnableWithWriterStore(Protocol[Input, Output]):
|
||||
def __call__(
|
||||
self, state: Input, *, writer: StreamWriter, store: BaseStore
|
||||
) -> Output: ...
|
||||
|
||||
|
||||
class _RunnableWithConfigWriter(Protocol[Input, Output]):
|
||||
def __call__(
|
||||
self, state: Input, *, config: RunnableConfig, writer: StreamWriter
|
||||
) -> Output: ...
|
||||
|
||||
|
||||
class _RunnableWithConfigStore(Protocol[Input, Output]):
|
||||
def __call__(
|
||||
self, state: Input, *, config: RunnableConfig, store: BaseStore
|
||||
) -> Output: ...
|
||||
|
||||
|
||||
class _RunnableWithConfigWriterStore(Protocol[Input, Output]):
|
||||
def __call__(
|
||||
self,
|
||||
state: Input,
|
||||
*,
|
||||
config: RunnableConfig,
|
||||
writer: StreamWriter,
|
||||
store: BaseStore,
|
||||
) -> Output: ...
|
||||
|
||||
|
||||
RunnableLike = Union[
|
||||
LCRunnableLike,
|
||||
Callable[Concatenate[Input, P], Output],
|
||||
Callable[Concatenate[Input, P], Awaitable[Output]],
|
||||
_RunnableWithWriter[Input, Output],
|
||||
_RunnableWithStore[Input, Output],
|
||||
_RunnableWithWriterStore[Input, Output],
|
||||
_RunnableWithConfigWriter[Input, Output],
|
||||
_RunnableWithConfigStore[Input, Output],
|
||||
_RunnableWithConfigWriterStore[Input, Output],
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.2.70"
|
||||
version = "0.2.74"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -2832,10 +2832,10 @@
|
||||
'''
|
||||
# ---
|
||||
# name: test_prebuilt_tool_chat
|
||||
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}, "BaseModel": {"properties": {}, "title": "BaseModel", "type": "object"}}, "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}, "structured_response": {"anyOf": [{"type": "object"}, {"$ref": "#/$defs/BaseModel"}], "title": "Structured Response"}}, "required": ["messages", "structured_response"], "title": "LangGraphInput", "type": "object"}'
|
||||
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}}, "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}}, "required": ["messages"], "title": "LangGraphInput", "type": "object"}'
|
||||
# ---
|
||||
# name: test_prebuilt_tool_chat.1
|
||||
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}, "BaseModel": {"properties": {}, "title": "BaseModel", "type": "object"}}, "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}, "structured_response": {"anyOf": [{"type": "object"}, {"$ref": "#/$defs/BaseModel"}], "title": "Structured Response"}}, "required": ["messages", "structured_response"], "title": "LangGraphOutput", "type": "object"}'
|
||||
'{"$defs": {"BaseMessage": {"additionalProperties": true, "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "properties": {"content": {"anyOf": [{"type": "string"}, {"items": {"anyOf": [{"type": "string"}, {"type": "object"}]}, "type": "array"}], "title": "Content"}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}, "id": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Id"}}, "required": ["content", "type"], "title": "BaseMessage", "type": "object"}}, "properties": {"messages": {"items": {"$ref": "#/$defs/BaseMessage"}, "title": "Messages", "type": "array"}}, "required": ["messages"], "title": "LangGraphOutput", "type": "object"}'
|
||||
# ---
|
||||
# name: test_prebuilt_tool_chat.2
|
||||
'''
|
||||
|
||||
@@ -35,6 +35,7 @@ from langchain_core.runnables import (
|
||||
RunnableLambda,
|
||||
RunnablePassthrough,
|
||||
)
|
||||
from langchain_core.runnables.graph import Edge
|
||||
from langsmith import traceable
|
||||
from pytest_mock import MockerFixture
|
||||
from syrupy import SnapshotAssertion
|
||||
@@ -53,6 +54,7 @@ from langgraph.checkpoint.base import (
|
||||
CheckpointTuple,
|
||||
)
|
||||
from langgraph.checkpoint.memory import InMemorySaver, MemorySaver
|
||||
from langgraph.config import get_stream_writer
|
||||
from langgraph.constants import CONFIG_KEY_NODE_FINISHED, ERROR, PULL, START
|
||||
from langgraph.errors import InvalidUpdateError
|
||||
from langgraph.func import entrypoint, task
|
||||
@@ -6416,3 +6418,112 @@ def test_tags_stream_mode_messages() -> None:
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_node_destinations() -> None:
|
||||
class State(TypedDict):
|
||||
foo: Annotated[str, operator.add]
|
||||
|
||||
def node_a(state: State):
|
||||
value = state["foo"]
|
||||
if value == "a":
|
||||
goto = "node_b"
|
||||
else:
|
||||
goto = "node_c"
|
||||
|
||||
return Command(
|
||||
update={"foo": value},
|
||||
goto=goto,
|
||||
graph=Command.PARENT,
|
||||
)
|
||||
|
||||
subgraph = StateGraph(State).add_node(node_a).add_edge(START, "node_a").compile()
|
||||
|
||||
# test calling subgraph inside a node function
|
||||
def call_subgraph(state: State):
|
||||
return subgraph.invoke(state)
|
||||
|
||||
def node_b(state: State):
|
||||
return {"foo": "b"}
|
||||
|
||||
def node_c(state: State):
|
||||
return {"foo": "c"}
|
||||
|
||||
for subgraph_node in (subgraph, call_subgraph):
|
||||
# destinations w/ tuples
|
||||
builder = StateGraph(State)
|
||||
builder.add_edge(START, "child")
|
||||
builder.add_node("child", subgraph_node, destinations=("node_b", "node_c"))
|
||||
builder.add_node(node_b)
|
||||
builder.add_node(node_c)
|
||||
compiled_graph = builder.compile()
|
||||
assert compiled_graph.invoke({"foo": ""}) == {"foo": "c"}
|
||||
|
||||
graph = compiled_graph.get_graph()
|
||||
assert [
|
||||
Edge(source="__start__", target="child", data=None, conditional=False),
|
||||
Edge(source="child", target="node_b", data=None, conditional=True),
|
||||
Edge(source="child", target="node_c", data=None, conditional=True),
|
||||
] == graph.edges
|
||||
|
||||
# destinations w/ dicts
|
||||
builder = StateGraph(State)
|
||||
builder.add_edge(START, "child")
|
||||
builder.add_node(
|
||||
"child", subgraph_node, destinations={"node_b": "foo", "node_c": "bar"}
|
||||
)
|
||||
builder.add_node(node_b)
|
||||
builder.add_node(node_c)
|
||||
compiled_graph = builder.compile()
|
||||
assert compiled_graph.invoke({"foo": ""}) == {"foo": "c"}
|
||||
|
||||
graph = compiled_graph.get_graph()
|
||||
assert [
|
||||
Edge(source="__start__", target="child", data=None, conditional=False),
|
||||
Edge(source="child", target="node_b", data="foo", conditional=True),
|
||||
Edge(source="child", target="node_c", data="bar", conditional=True),
|
||||
] == graph.edges
|
||||
|
||||
|
||||
def test_pydantic_none_state_update() -> None:
|
||||
from pydantic import BaseModel
|
||||
|
||||
class State(BaseModel):
|
||||
foo: Optional[str]
|
||||
|
||||
def node_a(state: State) -> State:
|
||||
return State(foo=None)
|
||||
|
||||
graph = StateGraph(State).add_node(node_a).add_edge(START, "node_a").compile()
|
||||
assert graph.invoke({"foo": ""}) == {"foo": None}
|
||||
|
||||
|
||||
def test_get_stream_writer() -> None:
|
||||
class State(TypedDict):
|
||||
foo: str
|
||||
|
||||
def my_node(state):
|
||||
writer = get_stream_writer()
|
||||
writer("custom!")
|
||||
return state
|
||||
|
||||
graph = StateGraph(State).add_node(my_node).add_edge(START, "my_node").compile()
|
||||
assert list(graph.stream({"foo": "bar"}, stream_mode="custom")) == ["custom!"]
|
||||
assert list(graph.stream({"foo": "bar"}, stream_mode="values")) == [
|
||||
{"foo": "bar"},
|
||||
{"foo": "bar"},
|
||||
]
|
||||
assert list(graph.stream({"foo": "bar"}, stream_mode=["custom", "updates"])) == [
|
||||
(
|
||||
"custom",
|
||||
"custom!",
|
||||
),
|
||||
(
|
||||
"updates",
|
||||
{
|
||||
"my_node": {
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,6 +6,11 @@ client.cjs
|
||||
client.js
|
||||
client.d.ts
|
||||
client.d.cts
|
||||
react.cjs
|
||||
react.js
|
||||
react.d.ts
|
||||
react.d.cts
|
||||
node_modules
|
||||
dist
|
||||
.yarn
|
||||
docs
|
||||
|
||||
@@ -10,10 +10,11 @@ function abs(relativePath) {
|
||||
}
|
||||
|
||||
export const config = {
|
||||
internals: [],
|
||||
entrypoints: { index: "index", client: "client" },
|
||||
internals: [/react/],
|
||||
entrypoints: { index: "index", client: "client", react: "react/index" },
|
||||
tsConfigPath: resolve("./tsconfig.json"),
|
||||
cjsSource: "./dist-cjs",
|
||||
cjsDestination: "./dist",
|
||||
additionalGitignorePaths: ["docs"],
|
||||
abs,
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user